From 90cf05f30b6848637892baa914095d10776dc30a Mon Sep 17 00:00:00 2001 From: Klappy Date: Mon, 20 Apr 2026 14:45:50 +0000 Subject: [PATCH] odd: P1.3.4 closeout ledger + handoff superseded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Writes the P1.3.4 closeout ledger for the encode canon-parity refactor (oddkit 0.23.0), and flips the P1.3.4 handoff to status: superseded pointing at the ledger. Closes the canon-parity sweep — all three tools (challenge, gate, encode) are now parity-clean. Ledger: odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed.md Supersedes: odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity.md Captures: - Second end-to-end application of release-validation-gate canon - Two Bugbot findings dispositioned as autofix fix-forwards - Two Sonnet 4.6 validators (feat + promotion) both PASS - Rule 3 resolution of 0.22.0 parallel-release version collision - P11 / P13 / L-08 carry-forwards; L-01 / L-02 new learnings --- .../2026-04-20-p1-3-4-encode-canon-parity.md | 3 +- ...04-20-p1-3-4-encode-canon-parity-landed.md | 143 ++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed.md diff --git a/odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity.md b/odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity.md index a098ece4..d1f3495b 100644 --- a/odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity.md +++ b/odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity.md @@ -12,7 +12,8 @@ date: 2026-04-20 session_span: "2026-04-20 post-P1.3.3 — fresh session handoff" derives_from: "odd/ledger/2026-04-20-p1-3-3-challenge-canon-parity-landed.md, odd/ledger/2026-04-20-p1-3-2-gate-canary-landed.md, canon/principles/cache-fetches-and-parses.md, canon/principles/vodka-architecture.md, canon/constraints/release-validation-gate.md, canon/principles/contract-governs-handoff-drift.md" governs: "Fresh-session continuation after P1.3.3 shipped oddkit 0.21.1. Points the next session at P1.3.4 — encode's canon-parity refactor: migrate the trigger-word matcher from regex alternation to stemmed set intersection (D5, same matcher shape as challenge + gate), and remove the module-level cachedEncodingTypes cache per D9 and cache-fetches-and-parses. Encode's trigger vocabulary is already governance-driven (read from odd/encoding-types/*.md at runtime); what remains is the matcher inside the classifier and the in-process cache on the parse products. This is the LAST regex matcher in the sweep. Ship as 0.22.0." -status: active +status: superseded +superseded_by: odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed.md --- # Handoff — P1.3.4 Encode Canon-Parity Refactor (0.22.0) diff --git a/odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed.md b/odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed.md new file mode 100644 index 00000000..f2c54086 --- /dev/null +++ b/odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed.md @@ -0,0 +1,143 @@ +--- +uri: klappy://odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed +title: "P1.3.4 Closeout — Encode Canon-Parity (D5 Phrase-Subset Matcher + D9 cachedEncodingTypes Removal), Plus Second Application of Release-Validation-Gate Canon, Plus the 0.22.0-Parallel-Release Version Collision" +audience: ledger +exposure: nav +tier: 3 +voice: neutral +stability: stable +tags: ["ledger", "p1-3-4", "encode", "canon-parity", "stemmed-matcher", "phrase-subset", "cache-removal", "cache-fetches-and-parses", "release-validation-gate", "second-application", "version-collision", "bugbot-autofix", "contract-governs-handoff-drift", "epoch-E0008.3", "sweep-completion"] +epoch: E0008.3 +date: 2026-04-20 +derives_from: "odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity.md, odd/ledger/2026-04-20-p1-3-3-challenge-canon-parity-landed.md, canon/constraints/release-validation-gate.md, canon/principles/cache-fetches-and-parses.md, canon/principles/vodka-architecture.md, canon/principles/contract-governs-handoff-drift.md" +complements: "canon/bootstrap/model-operating-contract.md" +governs: "Closeout record for P1.3.4 — the final step of the canon-parity sweep (oddkit_encode). Documents the shipped refactor, the second end-to-end application of the release-validation-gate canon, two Cursor Bugbot findings dispositioned as autofix fix-forwards, and the 0.22.0-parallel-release version collision resolved by canon-driven bump to 0.23.0. The canon-parity sweep is complete: all three tools (challenge, gate, encode) now use stemmed matching and have their in-process derivation caches removed." +status: active +supersedes: "odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity.md" +--- + +# P1.3.4 Closeout — Encode Canon-Parity (D5 Phrase-Subset Matcher + D9 cachedEncodingTypes Removal), Plus Second Application of Release-Validation-Gate Canon, Plus the 0.22.0-Parallel-Release Version Collision + +> P1.3.4 was scoped as a small canon-parity ship: migrate the last regex matcher in the suite to stemmed matching, remove the last in-process derivation cache, close the sweep. It accomplished all three — shipped as oddkit 0.23.0 (not 0.22.0 as the handoff called for, because parallel envelope-conformance fixes on main claimed 0.22.0 while this branch was in fresh-context validator dispatch). Along the way the release-validation-gate canon written in P1.3.3 operated end-to-end for the second time: two Bugbot findings caught and fix-forwarded in the same PR, a Sonnet 4.6 feat-validator dispatched returning PASS on all 5 corroborations, a separate promotion-validator dispatched returning PASS independently, and a version-collision-driven rebase resolved by Rule 3 (canon outranks session artifacts). The canon worked again on the session that inherited it. The orchestrator's session sandbox could not reach `*.workers.dev` at all due to egress proxy limitation — the validators literally had observation the orchestrator lacked, which is the exact scenario Rule 2 was designed for. One orchestrator-design-versus-autofix-design divergence produced a new learning candidate for graduation (L-08). + +--- + +## Summary — What Shipped, What Held, What Had to Bump + +P1.3.4 scoped two items per the handoff `klappy://odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity`: **D5** — migrate `oddkit_encode`'s trigger-word classifier from regex alternation to stemmed matching (same family as P1.3.1/0.21.0 and P1.3.2/0.20.0), and **D9** — remove the module-level `cachedEncodingTypes` / `cachedEncodingTypesKnowledgeBaseUrl` / `cachedEncodingTypesSource` in-process cache per `klappy://canon/principles/cache-fetches-and-parses`. The matcher shape ended up diverging from the handoff's suggestion (flat `Set`) to a phrase-subset variant (`string[][]`, all-stems-must-co-occur) after a Bugbot finding on the first cut. Both items landed via klappy/oddkit#126 (squash-merged to main as `7542cbb`) and promoted to prod via klappy/oddkit#130 (merged as `ea185a9`). + +The release-validation-gate canon held end-to-end on its second application. **Rule 1** (no merge with active reviews) caught two Bugbot findings on the feat PR before merge: a high-severity multi-word-vocab-flattening finding on commit `259170a` (fix-forwarded via Cursor autofix `113ba11`) and a low-severity `intersectsStems` dead-code finding on commit `113ba11` (fix-forwarded via Cursor autofix `e404fe0`). Both findings were real; neither would have been caught without Bugbot's fresh read. **Rule 2** (independent fresh-context validator) produced two Sonnet 4.6 read-only validator dispatches via Managed Agents — one against the pre-rebase feat-head `eaa1234` (agent `agent_011CaF5vo8B5UpqtfZAmSeui`, session `sesn_011CaF5vqjgzN7Mw8s84qvK9`) returning PASS on all 5 corroborations, and a separate one against the post-merge promotion head `7542cbb` (agent `agent_011CaF9tvJgRXQ6F96MtN4iu`, session `sesn_011CaF9tx18Af3z1Fy9trwz8`) also returning PASS on all 5 corroborations with the prod baseline cross-check additionally confirming that assertion (12) is the genuine fix introduced by D5 (prod 0.22.0 fails assertion 12; main preview 0.23.0 passes it, 223/0 × 3 runs). **Rule 3** (canon outranks session artifacts) resolved a real tension: the handoff said ship as 0.22.0, but while this branch was in feat-validator dispatch, PR #124 (telemetry_public envelope conformance) and PR #125 (catalog `debug.generated_at` envelope fix) landed on main and were released as 0.22.0 via PR #128 and promoted to prod via PR #129. Main-reality claimed 0.22.0 under different content. Per Rule 3, this refactor re-versioned forward to 0.23.0 — the handoff's "0.22.0" was session-scoped guidance; main-reality is the canon. + +The orchestrator's in-session observation was materially limited throughout. The sandbox egress proxy returned "DNS cache overflow" on every `*.workers.dev` HTTPS request for the entire session, which disabled direct preview and prod smoking from the orchestrator's hands. The fresh-context validators had different egress and performed all live curl + smoke corroborations unimpeded. This is the canonical case Rule 2's fresh-context requirement was designed for: the orchestrator literally could not observe what the validator needed to observe. + +The full sequence took roughly ninety minutes from P1.3.4 session open to PR #130 merged to prod. Both Bugbot findings, the rebase-for-version-collision, and a transient Test CF Preview check-run flake (that recovered to success without orchestrator intervention) were dispositioned under the canon without deviation. The canon worked again. The pattern that made P1.3.3 painful — Bugbot findings missed because the run was skipped, validator dispatch deferred under wall-clock pressure — did not recur. The infrastructure the canon names is now the default rhythm, not a discipline that has to be remembered. + +The canon-parity sweep is complete. All three tools in the oddkit epistemic suite now use stemmed matching (challenge: P1.3.3 / 0.21.0+0.21.1; gate: P1.3.2 / 0.20.0; encode: P1.3.4 / 0.23.0) and have their in-process derivation caches removed per `cache-fetches-and-parses`. The remaining structural step is P11 — `oddkit_gate` mechanical enforcement of release-validation-gate at execution→completion transitions — which is now the obvious next-epoch capability add given that all three tools are parity-clean. + +--- + +## Decisions + +**[D-01] Land D5 as stemmed phrase-subset matcher, not flat Set.** The handoff recommended flat `stemmedTokens: Set` matching the P1.3.3 challenge shape. The first cut shipped that (commit `259170a`). Cursor Bugbot immediately flagged it as a high-severity bug: multi-word canon phrases like `"committed to"`, `"going with"`, `"next step"`, `"blocked by"` were tokenized into individual stems and each stem was added to the flat Set. Stop-words are intentionally disabled on both sides of the tokenize call per P1.3.3 C-04 (so canon vocab like `from` and `with` survives), which meant `to`, `with`, `by`, `up`, `out`, `go`, `next`, `step` became standalone universal match triggers that would fire Decision and Handoff on virtually every English paragraph. The fix-forward landed via Cursor autofix commit `113ba11`, replacing `stemmedTokens: Set` with `stemmedPhrases: string[][]` (one inner array per canon vocab entry; single-word vocab degenerates to a one-element array; multi-word vocab preserves the stem sequence) and a new `matchesStemmedPhrases(phrases, inputStems)` helper that declares a type match only when ALL stems of at least one phrase appear in the input stem set. Single-stem phrases behave identically to the old singleton-set membership (inflection matching like `deciding` → `decid` still works); multi-stem phrases require stem co-occurrence so function-word constituents cannot fire on their own. + +**[D-02] Accept Bugbot autofix design over orchestrator's alternative.** The orchestrator proposed a stricter consecutive-subsequence match as an alternative to the autofix's subset match — a phrase would match only if its stems appeared in the input in the same order consecutively, preserving the pre-refactor regex `\b(committed to)\b` word-boundary semantic exactly. The autofix's subset-match is more permissive (stems must co-occur, any order, any distance). Both designs fix the Bugbot finding. The autofix was accepted because: (a) encode's design philosophy explicitly tolerates over-classification via the no-break multi-type path at `parseUnstructuredInput` L1226–1229 — the system is architected to emit multiple artifacts per paragraph when multiple types apply, so slight over-triggering is already the shape the system accommodates; (b) the subset-match is one uniform structure (a phrase is a list of stems, match means all-present) where the consecutive-subsequence variant required two code paths (singleton membership for single-stem phrases, ordered-subsequence for multi-stem phrases); (c) accepting the autofix over the orchestrator's alternative preserves the canonical Bugbot-as-fresh-context-reviewer disposition pattern — overriding autofix with a force-push would invert the review posture. This decision is also the basis for **L-08-candidate** below. + +**[D-03] Rebase forward to 0.23.0 per Rule 3 when main claimed 0.22.0.** The handoff said "Ship as 0.22.0" based on the state of main at handoff-writing time. While this branch was in fresh-context feat-validator dispatch (sessions can run 10+ minutes), PR #124 (telemetry_public envelope conformance, fixing E0008.2 appendix requirement) landed on main, PR #125 (catalog `debug.generated_at` response-time fix) landed on main, PR #128 (chore: release 0.22.0 — backfill CHANGELOG and bump version) landed on main bundling both under 0.22.0, and PR #129 (promote 0.22.0 to prod) merged to prod at 13:46:20Z. By the time the feat-validator returned PASS, main was at 0.22.0 with different content than what this refactor intended to ship. Under Rule 3 (canon outranks session artifacts), the handoff's "0.22.0" recommendation was session-scoped guidance; main-reality (0.22.0 already claimed under different content) is the canon. The canon-aligned resolution is a version bump, not a renegotiation. Rebased the feat branch onto current main, resolved a CHANGELOG merge conflict by placing the encode-D5+D9 content under a new `[0.23.0]` section above the existing `[0.22.0]` (telemetry + catalog), bumped `package.json` + `workers/package.json` + both lockfiles to 0.23.0, and added a version-note blockquote at the top of the `[0.23.0]` CHANGELOG entry explaining the collision and citing Rule 3. This is also the basis for **L-02** below. + +**[D-04] Dispatch two separate fresh-context validators, not one that gets re-queried.** The release-validation-gate canon's Rule 2 applies to promotion PRs specifically; the feat PR gets Rule 1 (Bugbot completed). But this sweep step touched load-bearing matcher code and the orchestrator's sandbox had observation limitations, so the orchestrator dispatched one validator against the feat-validated head `eaa1234` before merging to main (PASS) and a separate validator against the post-merge, post-rebase promotion head `7542cbb` before merging to prod (also PASS). The second validator additionally performed a prod-baseline cross-check (prod 0.22.0 smoke fails assertion (12); main preview 0.23.0 passes it), which is evidence that the shipped behavior change is the intended behavior change, not a test update without a matching semantic shift. This two-validator pattern is not canon (the canon requires one validator at promotion-PR time); it emerged as a hedge against session-latency scenarios where the pre-merge state might drift before the promotion PR opens. Whether to encode it as canon is open. + +**[D-05] Do not re-run failed Test CF Preview check manually; trust the validator's independent smoke.** The Test CF Preview check-run on promotion head `7542cbb` reported `failure` briefly before recovering to `success` without orchestrator intervention (no manual re-run dispatched). At the moment of the failure, the fresh-context promotion-validator was independently running `node test/canon-tool-envelope.smoke.mjs https://main-oddkit.klappy.workers.dev` three consecutive times and reporting 223 passed / 0 failed each run. The validator's observation was the ground truth; the Test CF Preview flake was the transient. Per Rule 1's conclusion hierarchy, a CI check that transitions to `success` has `success` as its conclusion, which is acceptable. No disposition action required beyond observation. + +--- + +## Observations + +**[O-01] Release-validation-gate canon held under pressure on its second end-to-end application.** P1.3.3 wrote the canon the same session it demonstrated the need for it, and ran the 0.21.1 fix-forward through the new canon as the first application. P1.3.4 is the second — a cleaner application, because the canon was inherited rather than written mid-stream. All three rules were binding, all three operated as designed, and neither Bugbot finding nor the version collision nor the egress limitation nor the Test CF Preview flake caused deviation. + +**[O-02] Both Sonnet 4.6 validators returned PASS independently with non-overlapping coverage.** The feat-validator ran the 5-corroboration pattern against `eaa1234` with preview URL `https://feat-encode-stemmed-matcher-d5-d9-oddkit.klappy.workers.dev` and reported: C1 PRD-vs-shipped (no scope creep, CHANGELOG accurate to what autofix shipped), C2 bytes-on-branch (zero functional refs to removed fields, `EncodingTypeDef` correct, no-break DESIGN comment preserved verbatim at L1226–1229, all 4 tokenize() call sites pass `new Set()` as second argument, cache reset lines deleted), C3 live curl × 3 on preview (all 15 calls consistent: inputs A-E × 3 runs, assertions 12–16 verified), C4 canon retrievability (all 7 `odd/encoding-types/*.md` URIs retrievable), C5 independent smoke × 3 (214/214 each run). The promotion-validator ran the same pattern against `7542cbb` with preview URL `https://main-oddkit.klappy.workers.dev` and reported all 5 corroborations PASS, with smoke 223/223 × 3 (the additional 9 assertions over the feat-validator's 214 came from the 0.22.0 telemetry+catalog envelope fixes that landed on main between the two validator dispatches). Prod baseline cross-check: same smoke against `https://oddkit.klappy.dev` (0.22.0) returned 222 passed / 1 failed, with the single failure being assertion (12) — the inflection-match anchor D5 is specifically designed to fix. This is direct evidence that D5 is the behavior change the test suite intends, not a matching-criterion change without semantic grounding. + +**[O-03] Two Cursor Bugbot findings both caught real bugs; both fix-forwarded within the feat PR.** Finding 1: high-severity on `259170a`, posted 2026-04-20T12:55:03Z, flagged multi-word vocab flattening. Root cause confirmed by orchestrator trace — stop-words disabled (required) + flat Set (naive) = function-word stems as universal triggers. Fix-forward: Cursor autofix commit `113ba11` on same day, rewriting `EncodingTypeDef.stemmedTokens: Set` to `EncodingTypeDef.stemmedPhrases: string[][]` with `matchesStemmedPhrases` helper. Finding 2: low-severity on `113ba11`, detected a now-unused `intersectsStems` helper left behind by the first-cut design and no longer called by `matchesStemmedPhrases`. Fix-forward: Cursor autofix commit `e404fe0` removing the dead-code helper. Both findings would have shipped silently pre-canon. Both are now anchored in the smoke regression harness — assertion (16) specifically anchors the phrase-subset fix (input `"I need to wait until tomorrow for the review"` must not classify as Decision or Handoff even though it contains `to`), and its failure mode would be the exact re-regression of the Bugbot finding. The P1.3.3 C-04 precedent (assertion 10/11 anchoring the `from`-keyword survival fix) held: numbered assertion named after the bug it prevents is now the standard pattern. + +**[O-04] Test CF Preview check-run went `failure` → `success` without orchestrator intervention.** On promotion PR #130 head `7542cbb`, the `Test CF Preview` check-run (running `bash tests/cloudflare-production.test.sh "$PREVIEW_URL"` against the main preview) initially reported `completed/failure` after 80 seconds of runtime. The orchestrator's egress proxy returned "DNS cache overflow" on direct preview probes, so live diagnosis from the orchestrator's hands was impossible. The fresh-context promotion-validator was independently running the same smoke file three consecutive times and reporting 223/0. Within the polling window, the Test CF Preview check-run re-transitioned to `completed/success` without any manual re-run dispatched (either by the orchestrator or by GitHub workflow re-run). Hypothesis: transient CF preview warm-up race with the CI runner's first smoke call. No intervention; the fail-then-succeed self-recovery indicates CI-side flakiness, not a code regression, which is corroborated by the validator's unbroken 223/0 × 3. Per Rule 1's conclusion hierarchy, the final `success` conclusion is the binding state. + +**[O-05] Orchestrator session egress was structurally limited; validator session egress was not.** The orchestrator's sandbox egress proxy returned `x-deny-reason: DNS cache overflow` on every HTTPS request to any `*.workers.dev` domain for the entire session. This made direct preview smoking, direct prod smoking, direct MCP self-calls against deployed oddkit, and direct workflow-log downloads from GitHub Actions logs (which redirect to Azure Blob) all impossible from the orchestrator's hands. The Managed Agents validator runs in a different container with different egress and reached all the same endpoints without failure — all 30 live curl calls against the feat preview, all 30 against the promotion preview, all 3 smoke runs × 223 assertions against each preview, all 7 canon-doc fetches, and all MCP self-calls against prod succeeded from the validator. This is the canonical case the release-validation-gate Rule 2 fresh-context requirement was designed for: the orchestrator literally could not observe what the validator needed to observe. Without the validator, this session would have had to either ship blind or abort, and the canon's insistence on independent fresh-context validation is what made the third option (ship with independent verification) possible. + +--- + +## Learnings + +**[L-01] Silent-matcher-semantics bug class — three Bugbot findings across P1.3.3 + P1.3.4 hit the same shape.** P1.3.3 finding: stop-word filter silently dropped canon vocab `from` from both the input side and the parse-time vocabulary side, making `\bfrom\b` inputs that matched the pre-refactor regex fail the post-refactor matcher, breaking the strictly-additive invariant without any test catching it. P1.3.4 finding 1: flat Set flattened multi-word canon phrases into individual stems and each stem became a standalone match trigger, silently firing Decision and Handoff on every English paragraph. P1.3.4 finding 2: the `intersectsStems` helper was left behind dead after autofix rewrote the matcher. All three are incorrect-matcher-semantics bugs producing silent behavior shifts that a test suite written without-the-bug-in-mind cannot anchor. All three required Bugbot's fresh read to detect. The regression-anchor pattern (numbered smoke assertion named after the bug it prevents) is now the documented standard: P1.3.3 shipped assertion (10) `from`-only source attribution and (11) `according to` multi-word, P1.3.4 shipped assertion (16) phrase-subset regression anchor. When a future sweep step ships a fourth matcher refactor (or revisits an existing one), the standing expectation is that Bugbot will find at least one similar-class bug, and the fix-forward disposition is to add a numbered regression anchor assertion before merging to main. This is graduation-candidate learning but the sample size across three findings over two sweep steps on load-bearing matcher code is already sufficient to name it — encoding here. + +**[L-02] When the handoff recommends a specific version and parallel work claims it first, the canon-aligned response is a version bump, not a renegotiation.** The handoff's "ship as 0.22.0" was based on the state of main at handoff-writing time. By the time the ship completed through the Rule 2 validator, main had moved. The wrong move would have been: try to force the encode ship into 0.22.0 (conflicts with the work that shipped as 0.22.0 on main and is already on prod), or revert PR #128/#129 to free up 0.22.0 (destructive, introduces prod-churn for no technical benefit), or waive the version conflict (produces two different code states under the same version number, guaranteed drift). The right move is bump. Bump is cheap (a version-number change in three files plus a CHANGELOG section), it preserves every other work product, and it respects main-reality as the canon state. The session-scoped recommendation ("0.22.0") was not wrong when written — it became wrong because reality moved. Canon outranks session recommendations. L-02 is adjacent to the existing `contract-governs-handoff-drift` principle but describes a different failure mode: `contract-governs-handoff-drift` covers handoffs that recommend shortcutting a canon rule; L-02 covers handoffs whose concrete-value recommendations become stale. Both point at the same hierarchy: canon > session artifacts. Whether L-02 graduates as a separate principle or folds into `contract-governs-handoff-drift` is open. + +**[L-03 — L-08 candidate] When orchestrator proposes a design and autofix proposes a different design that also fixes the finding, accept autofix unless canon requires the divergence.** On P1.3.4 Bugbot finding 1, the orchestrator's proposed fix was consecutive-subsequence phrase matching (stricter; preserves exact pre-refactor regex word-boundary semantic). Autofix's fix was subset-match phrase matching (more permissive; all-stems-must-co-occur any-order). Both fix the finding. The orchestrator-proposed design was more conservative and more precise; the autofix design was simpler and aligned better with encode's existing multi-type tolerance philosophy. Accepting autofix was the right call because: (a) Bugbot is the canonical fresh-context reviewer — overriding its autofix with a force-push would invert the review posture; (b) when two designs both fix the finding, the simpler one is usually correct under Occam; (c) the orchestrator can always contribute CHANGELOG narrative and regression-anchor assertions on top of autofix, keeping both participants in their canonical roles (Bugbot = fix, orchestrator = narrative + anchors). When the orchestrator can name a canon reason the autofix design is wrong — e.g., "autofix violates principle X" — then overriding is canon-aligned; without a named canon reason, accepting autofix is. This is not yet canon; it's the third or fourth time an autofix-vs-orchestrator-design tension has arisen, so calling it as a learning candidate for L-08 graduation in the next sweep step that touches a Bugbot-detectable matcher. + +--- + +## Constraints + +**[C-04-reinforced] `klappy://canon/constraints/release-validation-gate` held under pressure for the second time.** Rule 1 caught both Bugbot findings and prevented any merge-past-in-progress. Rule 2 produced two independent PASS verdicts with non-overlapping observation (one on pre-rebase feat head, one on post-rebase promotion head, different preview URLs, different validation environments). Rule 3 resolved the version collision without orchestrator hand-wringing. The canon's structural properties — enforced by governance, not by individual-session discipline — are what made this second application substantially easier than the first. P1.3.3 wrote the canon; P1.3.4 benefited from the canon already existing. The pattern that made P1.3.3 painful (Bugbot-skipped, validator-deferred, prod regression, 1h 39m window of broken `from` matching) did not recur. The canon did what structural fixes are supposed to do: it removed the individual-judgment-under-pressure failure mode by making the discipline mechanically unavoidable. + +**[C-P13 new] `parseUnstructuredInput` fallback to `types[0]` when no canon vocab intersects is pre-existing and outside P1.3.4 scope; future design decision needed.** Both P1.3.4 validators surfaced the same observation on assertion (16): the input `"I need to wait until tomorrow for the review"` is designed to NOT match any canon trigger vocab (the regression anchor is `!D && !H`), but the result is `[C]` (a single Constraint artifact) rather than `[]` (no artifacts). Root cause: `parseUnstructuredInput` has an existing fallback that emits one artifact of `types[0]` alphabetical (= Constraint, since the encoding-type letters sort `C < D < E < H < L < O`) when no type's matcher fires. This behavior predates P1.3.4 (present since 0.18.0 or earlier), was not modified by D5 or D9, and does not cause assertion (16) to fail (the assertion specifically tests `!D && !H`, not `artifacts.length === 0`). But it is a surprising default — users with unclassifiable input should arguably get back an empty artifact list or an explicitly-null-typed artifact, not a silent fallback to the alphabetically-first type. The design question is open: should fallback emit nothing? Emit a null-typed artifact with an explicit `classification: unknown` flag? Emit the fallback only when the input is a single paragraph (and `[]` for batch inputs)? This is not a bug that blocks P1.3.4 promotion; it is a latent design decision surfaced by the sweep's improved regression anchoring. Track as P13 carry-forward. + +--- + +## Handoffs + +**[H-01] Next session: encode architecture refactor — read DOLCHE vocabulary from canon at runtime.** This is the original next-step from the prior-session memory. The canon-parity sweep is complete, but `oddkit_encode`'s parser still hardcodes recognition of 4 types and collapses the full DOLCHE into a single artifact structure; the KB's DOLCHE vocabulary governance is not yet read at runtime. The sweep cleaned up the trigger-word matcher; the deeper architectural gap (parser disconnected from vocabulary governance) remains. Mirror the `telemetry_policy` canary pattern: read vocabulary governance from KB at runtime, return per-type artifacts, update tool description after resolution. + +**[H-02] P11 as the next structural step for release-validation-gate canon.** P11 was carried forward from P1.3.3 as "oddkit_gate mechanical enforcement of release-validation-gate at execution→completion transitions." Now that all three tools are canon-parity clean and the release-validation-gate canon has been applied end-to-end twice, P11 becomes the obvious next-epoch capability add — graduating the canon from documentation-plus-orchestrator-discipline to mechanically-enforced-by-the-gate-tool-itself. This is a capability expansion, not a matcher refactor; sits outside the canon-parity sweep; likely a new epoch. + +**[H-03] P13 — `parseUnstructuredInput` fallback-to-`types[0]` design decision.** See [C-P13]. Not a bug; a latent design question surfaced by the sweep. Candidate for a small minor version that makes the fallback behavior explicit (either change it or document it as intentional and add a `classification: fallback` flag). + +**[H-04] L-08 graduation candidate: "prefer autofix design over orchestrator design in Bugbot disposition unless canon names a reason for divergence."** See [L-03]. Call out on the next Bugbot autofix-vs-orchestrator-design tension; if it's the fourth occurrence, graduate as a tier-2 principle. + +**[H-05] Close the P1.3.4 handoff.** Flip `klappy://odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity` frontmatter to `status: superseded`, `superseded_by: odd/ledger/2026-04-20-p1-3-4-encode-canon-parity-landed.md`. + +--- + +## Encodes + +**[E-01] Full DOLCHE encode performed via `oddkit_encode` action=encode, saved to `/home/claude/work/encodes/p1-3-4-closeout.md` (local to orchestrator session, not persisted to knowledge base — the encode tool's output must be explicitly saved per standing rule).** Content mirrors the Decisions / Observations / Learnings / Constraints / Handoffs sections above. Canon governance source: `klappy://canon/definitions/dolcheo-vocabulary` (7 types: C / D / E / H / L / O / O-open). Parser quality scores ranged from 0/2 ("weak — expand and add rationale") to 2/2 ("adequate"); most artifacts scored 1/2 ("weak — add rationale") which is the expected floor for session-closeout encodes that re-use ledger narrative rather than compose fresh rationale. Ledger narrative in this document is the expanded form; the encode artifacts are the compressed form. + +--- + +## Timeline (UTC) + +| Time | Event | +|---|---| +| ~11:45 | P1.3.4 session opened; handoff `klappy://odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity` read; canon loaded. | +| 12:28 | PR #125 (catalog envelope fix) merged to main by parallel work. | +| 12:55 | Cursor Bugbot posted high-severity finding on `259170a` (multi-word vocab flattening). | +| 13:09 | Cursor autofix `113ba11` landed fix-forward; phrase-subset `stemmedPhrases` shipped. | +| ~13:16 | Feat-validator dispatched against `eaa1234` (sesn_011CaF5vqjgzN7Mw8s84qvK9). | +| 13:18 | PR #127 (main → prod promotion of catalog fix) merged to prod by parallel work. | +| 13:25 | PR #128 (chore: release 0.22.0 — backfill CHANGELOG + version bump covering #124 and #125) merged to main by parallel work. | +| 13:32 | Feat-validator returned PASS on all 5 corroborations; feat PR #126 ready to merge. Version collision surfaced — main at 0.22.0, this branch also at 0.22.0 under different content. | +| 13:46 | PR #129 (promote 0.22.0 to prod) merged to prod by parallel work. Prod now at 0.22.0 (telemetry + catalog). | +| ~13:47 | Rebase-merge decision per Rule 3: feat branch merges origin/main in; CHANGELOG `[0.22.0]` content moves to new `[0.23.0]` above main's `[0.22.0]`; all four version files bump to 0.23.0; merge commit `d2acf91` pushed. | +| 13:54 | Klappy-identity commit `8a0636be` fix-forwards a stale `0.22.0` → `0.23.0` reference in the `cleanup_storage` cache-removal comment. | +| ~13:58 | Bugbot re-runs on `8a0636be`; completes success; Rule 1 GREEN. | +| ~13:59 | Feat PR #126 squash-merged to main as `7542cbb` (0.23.0). | +| 14:00 | Promotion PR #130 opened (main → prod, 0.23.0). | +| ~14:01 | Promotion-validator dispatched against `7542cbb` (sesn_011CaF9tx18Af3z1Fy9trwz8). | +| ~14:03 | Test CF Preview check-run reports `failure` on `7542cbb` (transient). | +| ~14:12 | Test CF Preview check-run transitions to `success` (self-recovered); Bugbot completes success; Rule 1 GREEN. | +| ~14:26 | Promotion-validator returns PASS on all 5 corroborations; smoke 223/0 × 3; prod baseline cross-check confirms assertion (12) is the new-in-0.23.0 behavior. | +| ~14:28 | Promotion PR #130 merged to prod as `ea185a9`. | +| ~14:30 | DOLCHE encode performed; saved to orchestrator filesystem. | +| This ledger | Closeout written; handoff flipped to superseded. | + +--- + +## References + +- **Handoff superseded:** `klappy://odd/handoffs/2026-04-20-p1-3-4-encode-canon-parity` +- **Predecessor ledger:** `klappy://odd/ledger/2026-04-20-p1-3-3-challenge-canon-parity-landed` +- **Binding canon:** `klappy://canon/constraints/release-validation-gate`, `klappy://canon/principles/contract-governs-handoff-drift` +- **Principles applied:** `klappy://canon/principles/cache-fetches-and-parses`, `klappy://canon/principles/vodka-architecture` +- **Feat PR:** klappy/oddkit#126 (squash-merged `7542cbb`) +- **Promotion PR:** klappy/oddkit#130 (merged `ea185a9`) +- **Feat-validator:** agent `agent_011CaF5vo8B5UpqtfZAmSeui`, session `sesn_011CaF5vqjgzN7Mw8s84qvK9` +- **Promotion-validator:** agent `agent_011CaF9tvJgRXQ6F96MtN4iu`, session `sesn_011CaF9tx18Af3z1Fy9trwz8` +- **DOLCHE encode:** `/home/claude/work/encodes/p1-3-4-closeout.md` (orchestrator session only; not persisted to KB)