Repository navigation
Releases: MoebiusX/Observogram
Release list
v0.5.0 — the build-and-compare release
The build-and-compare release. A pack can now be built as well as inspected: the Build journey (Define, Compile, Verify) with a seed, an editor and spec 1.3. The studio stores its state in a database, with local and OIDC sign-in, roles enforced on every route, organisations and deploy profiles. The monitor-of-monitors work continues with early-warning delivery, the neuron view and inventory coverage. The studio was reviewed and redrawn: the pack operations in the header, Compare as Pack A beside Pack B, Discover as a board that judges nothing, one visual language as a design system downstream apps can vendor. And the use case the product stands on now holds: a repository scan and the live draft of the same system match, with what the live side could not look at reported as not checked instead of missing. Also: routes match case-sensitively (the 0.4.1 hotfix on main, #118, is the same fix).
The full entry is too long for a release body (GitHub caps it at 125,000 characters), so the newest section is below in full and the rest are listed by title. Every section is in docs/CHANGELOG.md at this tag, under 0.5.0.
Scan a repository, draft from live, compare: the same system now matches
The use case the product stands on — scan a service's repository, draft a pack from the live system it deploys, compare — reported 68% overlap on a real platform (253 of 371 artefacts in both, 88 "only in the repo", 30 "only live", 1 drifted). Almost none of that was drift. The two readers described the same system in different artefacts, and the comparison called everything the live side could not see "missing". On the same platform it is now 281 in both, all aligned, nothing "only live", 15 reported as not checked, and 38 only in the repo — metrics the repository declares or queries that have no series in production, each confirmed against the metrics store.
One reading for both sides. What a scan and a live fetch both read, they now read through one module (docs/MCP_INTEGRATION.md, One reading for the repository and for live):
- SLIs are inferred by rule name, not arrival order (
tools/lib/sli-inference.mjs). A repository lists rules in file order, a ruler in group order; taking "the first" recorded series made the same rule set infer a different expression for one SLI on each side — a false "drifted". - Alerting rules → burn-rate entries (
burnAlertsFromAlertRules, same module, moved out of the crawler). The live fetcher only recognised rules shaped like the compiler's own; a platform with seventy hand-written alerting rules read as having no burn-rate policy, so every alert its repository declares read "not live". A rule whose expression references a recorded series now binds to that series' SLO on both sides.for: 600sin a file andduration: 600from a ruler are one window (canonicalRuleDuration); a rule with noforno longer gets an invented5m. Entries read this way from live rules are real but not stampedVerified(mcp.derived.*names the rules). - Alertmanager routes (
tools/lib/alert-routes.mjs, moved out of the crawler). A newalerting_routesprobe reads the configuration the running Alertmanager reports with its status and takes the routes out of it — the live pack used to carry one invented SEV1 route. Secret addresses Alertmanager prints as<secret>stay channels withredacted:secret; a repo webhook that is only${VAR}stays a channel withunresolved:<VAR>(it used to be left out, so the route read "live, not declared"). The compiler sendsunresolved:<VAR>back out as${VAR}and references a redacted webhook byurl_file. Nothing else of the configuration is kept. - Backends are what runs, not what the MCP supports (
tools/lib/backend-products.mjs: one product table for container images and scrape jobs). The fetcher minted aVerifiedbackend for every row ofbackend_capabilities— a catalogue of what the MCP server can speak to. A platform running VictoriaMetrics read as also running Prometheus, Mimir, Thanos, Cortex, Cilium, Consul and Graylog (29 backends, 11 "extended surfaces"), while its collector, Promtail, node-exporter and vmalert — all with a scrape target up or a tool answering — were absent. A backend now needs evidence: a version the product reports, a scrape job of its own with a target up, or a product tool that answered (the registry's newattests). The catalogue still supplies version policy and stays inmcp.capabilities.*;mcp.capabilities.unobservedlists what is supported and was not seen,mcp.backends.evidencewhat attested each backend.
The comparison says "not checked" where it could not look (tools/lib/diff.mjs). The fetcher names every artefact family it had no way to observe in observogram.unobserved.<family> with the reason — no tool for it (the collector configuration, the SDK block), or the tool failed (a Grafana that refuses the login). An artefact the other pack holds in such a family goes to a new notObserved bucket instead of "declared, not live", outside the union. Seven declared dashboards behind an HTTP 401 are no longer seven missing dashboards.
Metrics pair as families (foldMetricFamilies, tools/lib/artefact-model.mjs). A histogram declared in source and the _bucket / _count / _sum series a store lists for it are one metric; so are a counter and the _total / _created it is exposed as. Paired name for name, the declaration read "not live" and its own series "live, not declared".
Smaller readings corrected. An SLI's good / total are normalised like any expression (a ruler returns rule bodies with a trailing newline). A Promtail or Alloy scrape_configs entry — log files to tail — is no longer declared as a metrics scrape job. Metrics a script writes in the exposition format (# TYPE name gauge) and pushes are declared like a client-library call (not from test files).
In the studio. Compare shows a Not checked in B group in each pane with the reason printed under it, a not checked chip on those cards and a sixth summary tile when there is something to say; every series of a metric family takes the family's status. A group's number is artefacts, as in the summary; when a metric spans several series the cards are said beside it (Only in A 38 · 44 cards). The Summary lists how many could not be checked. The live draft summary lists the backends found and, apart, what the MCP supports but was not seen running, the alerting routes read, and the families it could not observe.
For saved journeys. alerting_routes is a sixth probe family. A journey against an MCP that does not offer an Alertmanager status tool now lists it under not exposed (vantage restricted), and one whose status answer carries no configuration under failed (vantage partial), so its next run reports the vantage change once. Nothing about how a run is scored changed.
Goldens. tools/fixtures/golden-crawl.pack.json: the fixture's pure-${VAR} webhook route is now declared with unresolved:GOLDEN_HOOK_URL (one route added). tools/fixtures/mcp/adapted/alert_rules.json: the rule with duration: 0 loses the invented for: 5m. New: adapted/alerting_routes.json over synthetic/alertmanager_status.config.json. The compile goldens are unchanged.
Tests. tools/test-scan-live.mjs (in npm test) is the use case offline: one system written as a repository and as the answers an MCP gives for it — groups in another order, duration for for, <secret> for addresses, series for histograms — scanned, drafted and compared with nothing on one side only and nothing drifted; then one rule, one route and one metric removed from the live side, each of which shows, and only it. test-diff, test-crawl, test-fetch-live and test-contract-shapes cover the pieces.
Also in this release
- The studio in one visual language, as a design system downstream apps can vendor
- Fixed: Discover is a catalogue again — no verdict before there is a reference
- Fixed: the pack operations are back in the header, and Compare opens on Pack A beside Pack B
- Fixed: deploy target profiles keep no credential — per user, stripped like the remembered URL, cleared at sign-out
- The layers are named by what sits on them
- Discover: four views of an open layer
- The store, slice 3b: the identity API — users, orgs, members, the join role, sign out everywhere
- The store, slice 3a: roles enforced on every route, the live pack per org
- Fixed: routes match case-sensitively —
/API/…no longer slipped past the auth gate (#117) - The studio UX review: every screen leads with context, decision and one next action
- The Build journey's step copy
- The store, slice 2b: the offline operations — export, import --replace, rekey-issuer, purge-org
- The store, slice 2a: identity on the store — the boot order, the import, revocable sessions, tenancy always on
- Spec 1.3:
good_when— a threshold SLI's bound has a direction - The editor — one pop-up over an SLI and its SLO; the stack's cards open it; the sheet is live on Define
- The tier is a seed, the library values are copies, the definition is a wizard stage
- The pack is the axis — the definition column, the stack as the main surface, a sheet per layer
- The axis, after review — the sheet stays non-modal, the switches say what the engine fails, AA on the sheet and the column
- Build is a live scan — the layer stack on every Build step (foundation slice)
- The BUILD journey, slice 2: the studio journey and the API
- The BUILD journey, slice 1: the pack library, the tier scaffold and
packc init - Build number: the commit the studio runs from, in the footer, the CLI and /api/version
- Dashboards: symmetric generated boards — full rows, equal heights, balanced tiles, bound-SLO bar gauges
- Reference packs: the names the lab exposes (prometheus, grafana, kafka) and an honest cer...
v0.4.0 — the monitor-of-monitors release
The monitor-of-monitors release. Observogram assures that the artefacts which monitor a system keep doing their job: the live fetcher stops asserting what it never observed (steps 0-1), the stack samples its own self-metrics through the MCP (step 2), journeys keep stack-health evidence per run (step 3), and the requirement graph shows what would go blind when an artefact dies, a per-node ladder and candidate causes (step 4). The pack generators (boards including a Unified Observability board for every pack, burn-rate rules with the live-measured PromQL, gen-site for a fleet of environments from one pack and an inventory) became one vendorable library and the compiler emits the same PromQL. Also: stage 2 tenancy, first-boot admin and account menu, Diagnose views on the behavioural engine, the Tomograph to Observogram rebrand, legacy pack format support.
Blind-spot blast radius on the graph (step 4)
Step 3 kept the stack's own self-metrics per journey run; step 4 puts the monitor-of-monitors question on the requirement graph itself: what would go blind if an artefact died (structural exposure), whether a present artefact is doing its job or the vantage could not look at all (the per-node ladder), what changed since the previous run, and which candidate causes the evidence can offer. Three things are true of everything in this block, by contract and pinned by the suites: nothing scored changed — branch.integrity / verdict / counts, every node status, rollup.integrityMean and the grade's Drift-free criterion are byte-identical with and without the new annotations, everything new is an additive field beside them, and the switch to the ladder is a proposal (docs/SCORING_PROPOSAL_LADDER_INTEGRITY.md), not applied; the honesty vocabulary is kept — a blast radius says what WOULD go blind, never that something IS blind, unobserved means the vantage could not look (a probe family failed or is not exposed by the tier), never "absent", and present_unhealthy means on the wire but not doing its job; and causes are candidate causes ranked by evidence — not a root-cause verdict, with a change of the vantage itself reported separately and never as a cause. No new MCP tool name, no new dependency, no build step; the two new reasoning modules are zero-import and vendorable.
- Blast radius (reverse index). New zero-import
tools/lib/blast-radius.mjs:CONSUMER_SIDE(per edge type, which side consumes the other's assurance —fromforsli_of,sources,exported_by,produced_by,protects,forecasts,visualises,contains,routes,remediates,validates;toformaterialises, where the SLI consumes the rule's series),PROTECTION_SIDE(thetoside loses when thefromside dies: an SLO loses protection with its alert, an alert loses delivery with its route and remediation with its remediation — transitive, a dead route means an undelivered alert means an unprotected SLO),normalizeGraphShape(nodes as an array, a Map or a keyed object; edges withoutfrom/to/typedropped; edge types outside the table carry no known direction and are ignored),blastRadiusOf(shape, key, { weights })→{ key, kind, label, blinded: { total, weight, byKind, nodes: [{ key, kind, label, hop }] }, unprotected: { slos, alerts }, summary: { slos, alerts, panels, dashboards, routes, remediations, total } }(nullfor an unknown key; breadth-first over the reverse adjacency, cycle-safe;blinded.nodessorted hop →KIND_ORDER→ label and capped atBLINDED_NODES_CAP= 64 whiletotal/byKindstay uncapped; protection losers already blinded are not listed twice;weightsumsDEFAULT_WEIGHTS, a copy of the graph's limb weights, unknown kind 0.5) andblastRadiusIndex(shape)→Map key → summaryin sorted key order. Scaffold placeholders (ascaffoldflag orartefact.source === 'Scaffold') never blind anything, are never traversed and are never listed.traceability-graph.mjsexportsgraphShape(graph)— the artefact-free projection{ nodes: [{ key, identityKey, kind, layer, label, virtual, scaffold }], edges: [{ key, from, to, type, provenance }] }— andcompareBranchescomputes the A-graph and B-graph indexes once per comparison, attachingblastRadius: summary | nullto every node verdict (A's index for aligned / drifted / declared-only / unverifiable nodes, B's for live-only ones). - Per-node ladder (additive, unscored) + the scoring proposal. Every node verdict gains
ladder: { rung, status, detail }— rungsunobserved < absent < exists < alive < healthy, statuspresent_unhealthy|present_stale|unobserved|null— read from what the live fetcher wrote into Pack B:mcp.observed.scrape_targets/recording_rules/alert_rules, themcp.discovered.scrape_jobs_down/recording_rules_unhealthy/alert_rules_unhealthy/slis_unhealthylists,mcp.probesFailed/mcp.probesUnsupportedwithmcp.probeErrors.<family>,mcp.versions.<product>for backends andmcp.refreshedAtas "now" (a timestamp older than 2× the artefact's interval readspresent_stale; no interval or no timestamp means staleness is not judged). Burn-rate alerts link to live alert rules by the compiler's<slo>_burn_<factor>x_<short>_<long>convention, narrowed to the declared windows. Adeclared_onlyartefact the fetcher observed but withheld from Pack B (a scrape job whose every target is down) readspresent_unhealthy— on the wire beats absent; a kind whose probe family failed or is not exposed readsunobserved, never absent; anunverifiablekind reads rungunobservedwith statusnull; without anymcp.annotation every present node isexists. Per branch:ladderVerdict(broken>degraded>unobserved>healthy;undeclaredmirrors the scored fields) andladderIntegrity/ladderIntegrityPcton the same weights — unhealthy / stale credit 0.25, absent 0, unobserved nodes leave the denominator;rollup.ladder = { healthy, degraded, broken, unobserved, integrityMean, integrityPct }over the declared branches.docs/TRACEABILITY_GRAPH_COMPARISON_SPEC.md§5b carries the table and evidence rules;docs/SCORING_PROPOSAL_LADDER_INTEGRITY.mdproposes that Drift-free readrollup.ladder.integrityMeanundergradeSchema3, with the four worked rows on the test fixture — a proposal through the PHASE_1 acceptance path, not applied. - Per-run persistence: chains, versions, transition, live pack. New zero-import
tools/lib/chain-history.mjs:branchRecordsFromGraph(traceabilityGraph)keeps per branch{ rootKey, title, rootKind, verdict, ladderVerdict, integrityPct, ladderIntegrityPct, confidence, missingRoles, degraded, truncated? }with the degraded nodes (declared_only/drifted/live_only, or ladder statuspresent_unhealthy/present_stale/unobserved; aligned-and-healthy andunverifiablenodes are not recorded) worst first — absent, unhealthy, stale, drifted, unobserved, live-only, then widest blast radius — as{ key, kind, label, status, ladder, blastRadius: summary, deltaFields }(caps 64 branches × 16 nodes × 6 delta fields, a cut list markedtruncated);chainSummary(record)→{ declaredTotal, intact, partial, broken, undeclared, ladder: { healthy, degraded, broken, unobserved }, integrityPct, ladderIntegrityPct, degradedNodes, topExposure: { label, kind, slos, alerts } | null }(means arenullwith no declared chain — an empty set is not 100 % healthy;nullwhen the record carries nobranches);diffRunBranches(previous, current)→{ since, changed: [{ rootKey, title, from: { verdict, ladderVerdict }, to, direction, nodes: { newlyDegraded, recovered } }], appeared, disappeared, any }withdirectionfrom the rank tablesintact < partial < brokenandhealthy < degraded < unobserved < broken(worsewhen nothing improved and something got worse,betterthe mirror,changedotherwise or when either side isundeclared/ unknown;nullwhen either record carries no chains). The run record gainsbranches,chains,versions(mcp.versions.<product>→ value, bare product keys only,nullwhen none — an absence, never "unchanged"),transition(against the newest record read before this run is written;nullon the first run or after a record without chains, e.g. a vantage loss) andlivePack: { kept, path, bytes?, reason }. The definition keykeepLivePack: transitions | always | never(validated byloadJourneyDef; defaulttransitions) decides whether Pack B's canonical JSON is written toruns/<journey>/live/<record stem>.jsonbeside the record:transitionskeeps the first run, a run whose chains moved, a gate failure, the run after a vantage loss and the run after a record that cannot be compared; a file-sourced Pack B is never snapshotted (the file is the snapshot), whatever the policy. The purelivePackDecisionis exported; a snapshot write failure lands on the record ashistoryError, never thrown. Retention is unchanged (JOURNEY_RUN_FILE_REuntouched,readJourneyRunskeeps ignoringlive/) and now also removes orphaned snapshots — after the record prune,pruneLiveSnapshots(recordFiles, liveFiles)(pure) names the run-shapedlive/files whose record is gone, never a survivor's, never a non-run file;readLivePack(name, record)reads a snapshot back (nullwhen none was kept, the path is not thelive/<stem>.jsonshape, or the file is gone — a pruned snapshot reads as absent, never as an error).renderJourneyMarkdownadds aRequirement chainstable (chain | verdict | ladder | integrity | ladder integrity | worst node, cap 24, only when the record carries chains — no table means none were declared or recorded, never that every chain is intact; the worst-node cell carries the ladder detail andblinds N SLOs) and aTransitions since previous runsection (`title — intact/healthy → broken/broken (worse); newly degraded: …; recove...