Story-1475 - OBS-REGISTRY-DRIFT - pramaan-docs - Enforcement-hardening wave 1 (EPIC-1400)

Cross-repo registry drift guard

A runnable check that proves each executable repo's live observability registry surface is a subset of the canonical PRAMAAN registries, and fails CI on drift.

Why it matters

check_metric_registry.py only validates the canonical metric table's internal consistency. Nothing proved the actual span enum and metric specs shipping in functions / app / admin / agents still matched canonical — so a repo could add a span name, an extra metric label, change a unit, or carry a stale semconv source, and every per-repo test would still pass. That is the system-boundary defect: green in isolation while the cross-repo contract silently drifts.

This guard reads each repo's real registry surface and makes the drift fail CI, not production. It pairs the canonical docs with enforcement — a doc-only CI spec is insufficient without a runnable check.

What changed (pramaan-docs only, one PR)

No sibling repo is touched. No runtime behavior changes. ADR-124 is parsed read-only and never edited.

Mechanism: fetch-and-parse the source of truth

The guard fetches each repo's source-of-truth file and extracts the registry surface directly — Python via the standard-library ast module, TypeScript via a structural parse — rather than relying on a per-repo committed manifest.

OptionProCon
A — committed manifest per repodecoupled from source layoutneeds a PR in each sibling repo (forbidden cross-repo write); a stale manifest is itself invisible drift
B — fetch + parse (chosen)one repo / one PR; reads the source of truth, so no manifest-staleness gapextractor coupled to each surface shape — mitigated by frozen registry shapes and loud (exit 1) parse failures

Canonical span names come from the ADR-124 closed span table (anchored on the literal Closed manual span registry: sentinel); canonical metrics from the METRIC-REGISTRY:BEGIN/END markers in metric-registry.md.

Drift laws enforced

Evidence: green on live, red on each planted drift

Self-test (network-free fixtures) and the live check both pass; each planted drift driven through the full live pipeline (a local mirror of all six surfaces) fails with the right message.

ScenarioResult
Live registries (functions, app, admin, agents)PASS — 29 spans + 6 metrics, no drift
Planted out-of-registry span functions.rogue.leakFAIL — out-of-registry span name
Planted extra metric label session_idFAIL — extra metric label
Planted unit mismatch (s → ms)FAIL — unit mismatch
Planted stale semconv (OTel HTTP → OTel HTTP/1.0)FAIL — stale semconv key
Planted span-kind drift (INTERNAL → PRODUCER)FAIL — SpanKind mismatch
$ python3 .github/scripts/check_registry_drift.py --selftest
registry-drift guard SELFTEST PASSED: good fixtures clean; rogue span,
span-kind, extra label, unit, and semconv drifts all caught.

$ python3 .github/scripts/check_registry_drift.py --source gh
canonical: 34 span(s), 22 metric(s). source=gh
  pramaan-functions: 8 span(s)  | pramaan-agents: 12 span(s)
  pramaan-app: 5 span(s)        | pramaan-admin: 4 span(s)
  pramaan-functions: 4 metric(s)| pramaan-agents: 2 metric(s)
registry-drift guard PASSED: 29 span(s) + 6 metric(s) across 4 + 2 surface(s), no drift.

Compliance snapshot (2026-06-20)

All four repos are currently in compliance. Their live span/metric surfaces are a clean subset of canonical. The guard is green on live and red on each planted drift.

Not in scope

No changes to any sibling repo, no edit to ADR-124 or the metric registry table, no runtime/instrumentation code, and no board mutation. Required-attribute matching for spans is intentionally out of scope (the story covers name, unit, allowed-labels, and semconv).