Description
Found by SKT-0010.02 while refreshing the read-back corpus, 2026-08-27, and it will recur on every CLI upgrade.
CanonicalMerge includes source.collector_version in its identity check. For the gcx read-back producer that field tracks the gcx CLI version, not anything about the telemetry. It moved 1.1.1 to 1.2.0 between the committed corpus and the refresh, so a straight make signal-fidelity-eks-readback failed to merge — and overwriting instead would have silently deleted ten firehose families the committed document held that the new read-back window did not return.
That is a data-loss trap sitting behind a routine tool upgrade, and the merge is the one operation whose whole job is not to lose evidence.
Decision: the reading tool version does not belong in the merge identity. What identifies a corpus source is what produced the evidence — the substrate, the ingest path, the collector under audit — not which build of the CLI happened to read it. The tool version is provenance worth recording; making it part of the identity means every upgrade orphans the corpus. A capture producer like the k3d lab is different: there collector_version genuinely names the collector being audited, and it should stay in the identity.
The refresh this task was found by was bridged manually, running the real CanonicalMerge with the tool version held equal, so nothing was lost and every other merge semantic is intact. That bridge must not become the routine.
While in the same file, add the field SKT-0010.02 needs and could not add:
// InstrumentTypeSource names the mechanism this producer read instrument types from, or
// states why it could not observe them. It applies to every metric entry in the document
// and is the recorded reason behind any entry still carrying the unknown sentinel.
InstrumentTypeSource string `json:"instrument_type_source,omitempty"`
It needs validation that it is non-empty when present, preservation in the document clone, and a line in CanonicalMerge so a refresh can update the mechanism — otherwise the clone pins the old text forever. The exact values to stamp into each of the four corpus documents are recorded in the SKT-0010.02 notes and are final.
Acceptance Criteria
- #1 A read-back producer merges cleanly across a reading-tool version change, with no evidence lost
- #2 The reading tool version is still recorded as provenance, just not as merge identity
- #3 A capture producer whose collector_version names the audited collector keeps it in the identity
- #4 A test covers the version-bump merge, since this failed silently in the direction of data loss
- #5 CorpusSource carries InstrumentTypeSource, validated, cloned, and updatable on refresh
- #6 All four corpus documents carry their recorded instrument-type mechanism
Definition of Done
- #1 make gate (build vet test race rw-proto-check spdx-check forbidden-words)
- #2 make blueprint-schema (only if a blueprint field or construct/workload config struct changed)
- #3 DRY_RUN=true go run ./cmd/synthkit -once -dump — inventory diffed against signals/
Implementation Notes
ADDITIONAL REQUIREMENT from SKT-0013.04, and it now BLOCKS corpus promotion for the permutation matrix.
The matrix produces per-permutation capture candidates but can promote NONE of them, because corpusIdentity is (area | source.kind | source.substrate) and matchingCorpusIdentity rejects a merge whose kind or substrate differs. Two permutations of the SAME producer on the SAME substrate therefore collide: LoadCorpusDir rejects the second as a duplicate identity, and CanonicalMerge would fuse two deliberately different collector configurations into one document — silently destroying the distinction the matrix exists to capture.
Exact request, to land alongside this task existing merge-identity rework since both edit corpusIdentity and two independent edits will conflict:
- Add Permutation string
json:"permutation,omitempty"to CorpusSource. - Include it in corpusIdentity as (area | kind | substrate | permutation), in matchingCorpusIdentity check list, and in compareCorpusDocuments ordering tuple after Source.Kind.
- An ABSENT value must keep meaning today single-permutation document, so every existing file stays valid unchanged.
- loadCorpusFile uses DisallowUnknownFields, so the struct change MUST land before any document carrying the field, or every corpus load fails.
- validateCorpusDocument should normalise it (lowercase, no whitespace) so two spellings of one permutation do not fork the identity.
- ScopedFinding must carry it through CompareCorpus and the report must print it beside substrate and source kind — otherwise a finding cannot be attributed to a permutation, which makes the combined report unusable for its main purpose.
- reality-corpus/README.md should state that two permutations disagreeing about a metric family is a REAL PROPERTY OF THE ESTATE and never a contradiction to reconcile.
DELIVERED 2026-08-27, commit 1f3b433, alongside SKT-0013.01 which needed the permutation field.
The role discriminator
The merge identity could not simply drop collector_version: for the k3d lab it genuinely names the audited chart, and dropping it would fuse two chart versions’ evidence — the same class of harm this task exists to prevent, in the other direction. So CorpusSource gains a REQUIRED source.collector_role of “audited” or “reader”, and matchingCorpusIdentity appends the version check only for “audited”. All four committed documents are stamped: both k3d_lab documents audited, both gcx_live_readback documents reader.
Rejected: inferring the role from source.kind. A hard-coded kind list is invisible at the call site and silently mis-classifies the next producer.
Permutation
corpusIdentity is now (area | kind | substrate | permutation); permutation joins matchingCorpusIdentity and compareCorpusDocuments after Source.Kind. An absent value keeps the pre-permutation identity exactly, so every committed file stays valid unchanged — pinned by TestAbsentPermutationKeepsTheSingleDocumentIdentity.
FOUND WHILE PROMOTING, and fixed here because it makes the field usable rather than merely
present: the comparator had no permutation awareness, so the first permutation-tagged document
produced a FALSE contradiction. Measured, not theoretical — the OTel permutation’s pod logs
carry k8s.cluster.name where the Alloy permutation carries cluster and app_kubernetes_io_name,
and synthkit correctly emits the Alloy shape. dispositionAgainstPermutation demotes every
finding from a permutation-tagged document to coverage evidence; reality-only findings stay,
because what a permutation produces and synthkit does not is real coverage information.
Forward path recorded in the function comment: when a lane claims to emit a specific
permutation, synth declares which one and contradictions come back for that one.
InstrumentTypeSource
Added, validated non-blank when present, and updatable on refresh — CanonicalMerge takes the candidate’s value when non-empty, otherwise the clone would pin the first-written text forever. The gcx producer stamps it from a const in gcx_readback.go rather than a literal in four files.
Verification
TestCanonicalMergeSurvivesReadingToolVersionBumpWithoutLosingEvidence covers the 1.1.1 -> 1.2.0 bump that failed silently toward data loss: the merge now succeeds, the union keeps the family present only in the committed window, and the refreshed tool version is recorded as provenance. TestCanonicalMergeStillRejectsAnAuditedCollectorVersionChange covers the mirror case.
make gate green, 239 packages. make signal-fidelity: 68 contradictions before and after, so stamping four documents and reworking the identity changed no finding.
Final Summary
The reading tool’s version is out of the corpus merge identity, and the corpus can now hold more than one permutation of a producer. CorpusSource gains a required collector_role: an audited collector’s version stays identity because two chart versions are two producers, while a reader’s version is a CLI build that must not participate — left in, a routine gcx upgrade rejected the merge, and overwriting instead would have silently deleted the ten firehose families the newer read-back window did not return. source.permutation joins the identity so two configurations of one producer on one substrate stay separate documents. Fixing the field surfaced that the comparator had no permutation awareness: the first tagged document produced a false contradiction, because synthkit correctly emits the Alloy pod-log shape and the OTel permutation legitimately differs. dispositionAgainstPermutation now demotes findings from a tagged document to coverage evidence while keeping every reality-only finding. source.instrument_type_source is added, validated, refreshable, and stamped on all four documents. Verified by a test covering the version-bump merge in the data-loss direction, its audited mirror, and both permutation directions; make gate green at 239 packages, and contradictions unchanged at 68 before and after.