Story-1417 pramaan-admin EPIC-1400 trace graph migration (Phase-2) pramaan-docs ADR-124 §D10

A gate that can't be sweet-talked

The admin observability boundary was correct but only half-enforced: one vitest test pinned span names and attribute keys, yet nothing stopped a future agent from spinning up a second TracerProvider, calling registerOTel twice, or quietly adding a baggage propagator that would leak firm_id out the egress. This Story turns the helper boundary into a closed gate — raw-tracer/provider ban, an explicit "baggage stays absent" assertion, and the stale-key cleanup — and proves it bites with planted negatives.

What changed

One enforcement file grew teeth and two stale artifacts were corrected. tests/unit/static/manual-span-semconv.test.ts — the gate already wired into CI as pnpm run check:semconv — now runs four rules instead of two, each as a pure checker so it can be exercised against both the real src/ tree and synthetic offending input. No new script, no new CI job: the existing check:semconv step was simply widened (its CI step name and comment updated to match).

Raw tracer / provider ban

findForbiddenInstrumentation rejects startActiveSpan / start_as_current_span / start_span / getTracer / tracer.*, provider construction (*TracerProvider(, MeterProvider(), global setters (set_tracer_provider / setGlobalTracerProvider …), and any extra registerOTel( — everywhere in src/ except the two approved homes: src/instrumentation.ts (the one provider owner) and src/lib/observability/manual-span.ts (the one withManualSpan site). Every alternative is paren-anchored so prose mentions in comments do not false-positive.

Baggage stays absent

Admin runs propagators: ["tracecontext"] only — no baggage propagator, therefore no egress stripper. findBaggagePropagatorViolation fails CI the instant src/instrumentation.ts gains a baggage propagator unless an approved stripBaggageFromHeaders stripper also ships, mirroring the pramaan-app R12 propagator-config rule (ADR-124 §D10). This is the assertion form of the corrected scope: admin has no baggage today, so the full R12 helper is not needed — CI just guards the posture.

Stale semconv key fixed

The admin registry carried the pre-stable OTel key http.method. It is renamed to http.request.method in both the closed-registry set in the test and the helper's ADMIN_SPAN_ATTRIBUTE_ALLOWLIST / AdminSpanAttributes type. withManualSpan has zero product callers yet, so the rename is contained. A belt-and-suspenders assertion keeps the dead key from ever returning.

False comment corrected

src/lib/observability/manual-span.ts claimed the gate lived in scripts/check-manual-span-semconv.mjs — a file that never existed. The comment now points at the real gate: the vitest static test run via pnpm run check:semconv, and lists what it now enforces.

Why a pure checker, not a raw grep

The pramaan-app and pramaan-functions gates are shell grep pipelines — correct, but you can only prove they bite by committing a violation and watching CI go red. By factoring each rule into a pure function (findForbiddenInstrumentation, findBaggagePropagatorViolation, findRegistryViolations), the same gate that scans real src/ can be fed synthetic offending strings in-process. That makes the planted negatives permanent regression coverage: if a later refactor weakens a pattern so a violation slips through, the negative test goes red on its own, not just when someone happens to commit bad code. Control tests sit beside them — the same raw call inside an approved file is allowed, and a baggage propagator is allowed once a stripper ships — so the gate is proven specific, not just strict.

Planted negatives — the gate must go red

Each rule was proven two ways: a permanent in-suite negative test feeding the pure checker synthetic input, and a live verification that plants a real violation in the actual tree, runs pnpm run check:semconv, observes the failure, and reverts.

Planted violationRuleLive check:semconv result
Raw trace.getTracer().startActiveSpan() in a product file1RED — exit 1, "Raw tracer / provider calls found outside approved files"
new NodeTracerProvider() in a product file1RED — exit 1, same rule-1 failure
A second registerOTel({ … }) bootstrap1RED (in-suite negative)
baggage added to propagators in instrumentation.ts, no stripper2RED — exit 1, "A baggage propagator requires an approved stripBaggageFromHeaders egress stripper"
"matter.content" added to the helper attribute allowlist4RED — exit 1, "Attribute key(s) not in the ADR-124 admin registry: matter.content"

Verification proof

Gates green on clean code: pnpm exec tsc --noEmit (exit 0), pnpm run lint (0 errors; 7 pre-existing warnings in unrelated test files), pnpm run build (compiled successfully, all routes), pnpm run test:unit (529 passed across 86 files), and pnpm run check:semconv (14 passed — 5 live-src + 9 planted-negative/control).

Gate bites on dirt: each of the five planted negatives above drove check:semconv to exit 1 with a rule-specific message, then was reverted — git status clean afterward, no planted file or stray edit left behind.

Scope honesty: the corrected 2026-06-20 scope confirmed admin has no baggage propagator, so no R12 helper/stripper ships — only the CI assertion that baggage stays absent. Instrumentation-exempt (guardrail-only) per ADR-124; no new executable product workflow was added, so no new span was required.

One repo, one PR. Only .github/workflows/ci.yml, src/lib/observability/manual-span.ts, tests/unit/static/manual-span-semconv.test.ts, and this walkthrough changed.