SummaryWhat's Wrong, How to Verify, What Breaks
The bug: discoverEncodingTypes reads all canon articles tagged encoding-type and produces a registry keyed by letter. The dedup pass at line 512 keeps the first article per letter, with iteration order being alphabetical-by-path. Because odd/encoding-types/observation.md sorts before odd/encoding-types/open.md, Observation wins the O slot. Open's qualityCriteria array is silently discarded. The parser still recognizes the [O-open] prefix tag (facet handling lives elsewhere), and the artifact still emits with facet: "open", but the scoring step looks up criteria by letter alone — which means Open artifacts are scored against Observation's four criteria, not Open's five.
Verification path (reproducible): Call oddkit_encode with input [O-open P1] <any 10+ word body>. The returned artifact carries facet: "open" and priority_band: "P1" correctly. Inspect quality.maxScore. If the bug is still live, it returns 4 — Observation's max. If the fix has shipped, it returns 5 — Open's max (Substance, Priority assigned, Closure path, Specificity, Owner clarity).
What this breaks: Open artifacts get adequate quality scores too easily. Observation's criteria don't include "Priority assigned" or "Closure path" — the two criteria that distinguish a properly-recorded Open from a vague one. Operators using encode for Open items aren't getting the gap messages that would push them to add priority bands and closure paths. Net effect: Opens are recorded but recorded loosely, defeating the purpose of the seventh letter.
Why This Wasn't Caught Earlier
The bug landed when open.md was added to canon in PR #157 (E0008.4 Phase 1, merged 2026-04-30T05:08:53Z). Before PR #157, open.md either didn't exist or didn't register letter O, so the dedup logic never had to decide. The Phase 1 PR's canon migration didn't touch discoverEncodingTypes, and its closeout ledger didn't probe Open's quality scoring against expected output. The Audit 2026-04-30 (run within the hour after Phase 1 merged) caught it via direct tool call as part of inventorying actual code state vs. claimed code state.
This is a worked example of why the code-claims-require-code-observation principle matters: the closeout ledger for Phase 1 claimed Open had been "enriched in place" and was now part of the registry. That claim was true at the canon layer and false at the runtime layer. Only direct observation surfaced the gap.
Where the Fix Lives
klappy://odd/handoffs/2026-04-30-encode-vodka-refactor-alternative-d-revised Item 2:
Fix: Dedup by
(letter, facet)pair. Readfacetfrom frontmatter on each encoding-type article. Keep Observation as(O, undefined)or(O, "closed"); keep Open as(O, "open"). Scorer selects criteria by full pair, matching thefacetfield set by the prefix parser on each parsed artifact.Acceptance: Same
oddkit_encodecall with[O-open P1]input now returnsmaxScore: 5and applies Open's five criteria. Observation calls ([O]prefix or unstructured) continue to apply Observation's four. Regression test added against the canon URIs that surfaced the bug.
Phase 2 PR target: oddkit 0.28.0. Full release-validation-gate applies.
What Other Bugs This Pattern Could Hide
The dedup-by-single-key pattern almost certainly recurs elsewhere in the codebase. Worth spot-checking during Phase 2:
- Any registry built by
entries.filter(...).reduce(...)keyed on a single field where a tag-pair or pair-key is the actual identity. - Any
Map(types.map(t => [t.letter, t.name]))pattern in the parsers — same shape, same trap. - Any future encoding type that legitimately needs to share a letter with another type (the DOLCHEO design admits one such collision, Observation/Open; future types should follow the same
facetpattern).
If Phase 2 surfaces a second instance of the same bug shape, that's evidence for graduating the pattern to a tier-2 constraint (klappy://canon/constraints/registry-keys-must-include-disambiguating-facets or similar).
See Also
- Code Claims Require Code Observation — the principle that surfaced this bug
- Encode Current State (2026-04-30) — what the worker actually does today
- Phase 2 Revised Handoff — where the fix is scoped (Item 2)
- Audit 2026-04-30 Cleanup Ledger — the audit closeout
- Encoding Type: Open — the type whose criteria are being dropped
- Encoding Type: Observation — the type whose criteria are being substituted