Task · SKT-0010.01

Comparator evidence semantics: absent evidence is a gap, never a contradiction

Description

Owns internal/inventory/diff.go and the corpus schema fields the rule needs. Removes the three verified false-positive classes recorded on the parent without weakening the comparator anywhere a real divergence lives.

Three rules to implement, each with the exception preserved:

Unobserved instrument type. A corpus metric whose instrument_types is exactly the unknown sentinel carries no evidence about instrument shape. Comparing it must yield a coverage gap and a PENDING, never a contradiction. A corpus entry that DOES record a real instrument type must still contradict when synth disagrees — this rule must not become a blanket instrument-comparison bypass, which would silently hide the single defect class SKT-0006 was created to catch.

Read-path enrichment labels. asserts_env, asserts_site, service and __aggregation__ appear on gcx read-back entries because Grafana Cloud adds them after ingest. They are properties of the read path, not of collector egress, so they must not count as labels synthkit is missing. Declare them in the corpus source block with provenance (which producer sees them and why) rather than hard-coding a list in the comparator, so a future producer that does not add them is not silently governed by another producers quirk. The reverse direction is untouched: a label key present in synth and absent from reality is still a contradiction, because that is the never-invent-a-name rule.

Label-value subset. A capture from one account in one region observes a subset of the value space synthkit deliberately models. synth ⊇ reality is correct and must be silent. A contradiction is only reality ⊄ synth — reality carrying a value synthkit cannot produce. Where the corpus already marks a label values_elided: true there is no value evidence at all and no value comparison should run.

The distinction between the Contradictions and Coverage-gaps sections of the report is the load-bearing output: after this task the Contradictions section must contain only findings a maintainer would act on.

Acceptance Criteria

Definition of Done

Implementation Notes

DELIVERED 2026-08-27 (lane L1). internal/inventory/{diff.go,corpus.go} + tests, docs/reality-corpus.md, and enrichment-label declarations in the two eks-live-readback corpus source blocks.

Results, measured A/B against one fixed synth inventory (the first baseline was contaminated by lane L3 landing mid-run)

TOTAL 1845 -> 1049. Contradictions 866 -> 301. Coverage gaps 979 -> 748.

The three rules, and the exceptions that make them safe

The merge property matters more than it looks: CanonicalMerge unions declarations by key with existing provenance winning, so a producer re-run that omits the block never drops it. That is what let lane L2 regenerate the corpus concurrently without needing L1 files.

What the lane correctly refused to decide

Two classes, 223 of the 301 remaining contradictions, both outside the three rules it was given. It returned the questions rather than inventing answers, which was the right call.

Both routed to lane L12.

Genuine findings surfaced that were not the task

Of the 26 surviving label_value_contradictions, 11 are genuinely actionable fixture gaps (reality runs Graviton instance types synth does not model; created_by_kind AutoscalingListener/EphemeralRunner; host_network=true; kubelet operation_type values) and 7 are the le rendering class.

CONTINUATION DELIVERED 2026-08-27 (lane L12): the two root decisions applied, plus the le question resolved and a cross-lane breakage fixed.

Contradictions 304 -> 89 on L12 measured baseline, and -> 81 once the enrichment-values JSON landed (applied by the wiring pass). Note L1 reported 1049/301 but that is NOT reproducible: lane L2 was regenerating the corpus and lane L3 had emitter edits in the tree, so both sides moved. L12 measured its own before/after on one tree minutes apart, which is the honest comparison.

ITEM 1 — blueprint selector exempted, declaration-driven. Provenance gains SelectorLabels, stamped by a new cmd/synthkit/inventory.go from the RUNNER CONSTANT rather than the literal string, so a rename cannot silently reopen it. internal/inventory cannot import internal/runner (import cycle via the otlp sink), so the composition root is the only correct home. Exactly -215 rows, all blueprint-only. Every row carrying a real invented dimension survives, now with blueprint stripped from the reported synth set so the line reads cleanly. A test proves it is declaration-driven, not a hard-coded key.

ITEM 2 — enrichment VALUES, and the semantics are deliberately not a blanket widening. No values means key-scoped (whole key dropped, L1 behaviour). WITH values means value-scoped: the key STAYS and still compares as a key, and only the listed values are removed before value comparison. So declaring ip_family cannot silently stop the comparator noticing that synthkit never emits ip_family. Merge unions values but a key-scoped declaration on either side stays key-scoped, since that is the broader claim and merging must never narrow it. -8 exactly.

ITEM 3 — RETURNED AS A QUESTION rather than shipped, with numbers, and the root accepted the lane position. Declaring provenance.substrate = eks WOULD remove 38 contradictions, but the filter is document-level so it drops both k3d documents whole: 31 of the 38 are REAL — 24 invented-dimension rows on coredns, the 7 le rows — and the entire signals/k8s-addons.md area goes dark along with the two OTLP-logs coverage-gap findings. Net-negative. The synth export now carries provenance with Substrate deliberately EMPTY, documented and pinned by a test so nobody sets it by accident. Correct diagnosis: the five kubernetes_build_info rows are capture-instance identity, not a substrate problem, and the gcx producer ALREADY elides those values while the k3d lab does not. Tracked as SKT-0010.12.

ITEM 4 — RESOLVED, and synthkit is the wrong side. Prometheus v3 normalises le to a float-like representation on ingestion; Alloy 1.x embeds v3 scrape, so everything it scrapes reaches Mimir dot-zero. Corroborated from the corpus: 154 le values, ZERO bare integers; non-integral bounds match exactly while only integral ones diverge; bucket_bounds produce zero mismatches, so only the label STRING differs. Tracked as SKT-0010.11 — roughly 120 LEBare call sites across 21 files, so the 7 findings are only the visible subset.

CROSS-LANE BREAKAGE FIXED: make signal-fidelity was exiting 1 on the tree because LoadCorpusDir recursively parsed every .json under the corpus root as a corpus document, and SKT-0010.04 had just written reality-corpus/verdicts/. The walk now prunes subdirectories that are not signals areas. That location is load-safe — do not “fix” it by moving the verdict file.

Report readability: every finding line now leads with the one-sided difference that produced it, then both full sets. Without it a maintainer diffs two 15-element sets by eye on every labels row.

Each new rule was mutation-tested back to prove its tests fail for the right reason.

Final Summary

The fidelity report is readable. 1806 findings and 866 contradictions became 659 and 81, and every surviving contradiction is one a maintainer would act on — enumerated group by group in the notes with a verdict each.

Three evidence rules, each with its exception deliberately preserved. An instrument_types of exactly the unknown sentinel is absent evidence and becomes a coverage gap with a PENDING, while an entry recording a real type still contradicts. Read-path enrichment labels are declared per corpus source with provenance rather than hard-coded, stripped from the reality side only — a key synthkit emits that reality lacks still contradicts, which is the never-invent-a-name rule. Label values compare as a subset, so a single-account capture no longer fires 293 false positives.

Two further decisions were returned by the lane rather than invented, and taken by the root. synthkit own blueprint selector is exempted, sourced from the runner constant so a rename cannot silently reopen it — 215 rows, all of which reality can never carry by design. The Adaptive Metrics MARKER VALUE gets the same treatment as its sibling key, with value-scoped declarations that keep the key comparing so a genuinely missing dimension is still caught.

A third was measured and REFUSED: declaring the synth substrate would have removed 38 contradictions but 31 of them are real, taking the whole k8s-addons area dark. The synth export now carries provenance with substrate deliberately empty, pinned by a test.

The le question resolved against synthkit: Prometheus v3 normalises le on ingestion, the corpus carries 154 le values and zero bare integers, and an le=“1” selector matches nothing against real data. Tracked as SKT-0010.11 — roughly 120 call sites, so the 7 findings were the visible subset.

Also fixed a live breakage: the corpus loader parsed every .json under the root as a corpus document, so a sibling lane new verdicts directory broke make signal-fidelity outright. Each new rule was mutation-tested back to prove its tests fail for the right reason. make gate green.

View the source file on GitHub