Skip to content

promote 0.21.0 to prod (P1.3.3 challenge canon parity)#121

Merged
klappy merged 1 commit intoprodfrom
main
Apr 20, 2026
Merged

promote 0.21.0 to prod (P1.3.3 challenge canon parity)#121
klappy merged 1 commit intoprodfrom
main

Conversation

@klappy
Copy link
Copy Markdown
Owner

@klappy klappy commented Apr 20, 2026

Promotes oddkit 0.21.0 from main to prod.

What ships

Attestation

Stage Result
Preview branch smoke @ challenge-d5-d9-parity-oddkit 170/170 × 3 consecutive (after 1 cold-start flake)
CI Test CF Preview on PR #120 success
Main-preview smoke @ main-oddkit 170/170 × 3 consecutive

Same Option A smoke-heavy attestation pattern as P1.3.2 (no Sonnet 4.6 validator dispatch this round — scope is small, no envelope changes, no new governance files).

Sweep status after promotion

Tool Version Matcher BM25 cached?
oddkit_encode 0.18.0 regex (P8 follow-up) parse product cached
oddkit_challenge 0.21.0 stemmed set intersection + structural rebuild inline
oddkit_gate 0.20.0 BM25 + stemmed set intersection rebuild inline

Challenge has full canon parity with gate's matcher and caching discipline.

Refs


Note

Medium Risk
Changes oddkit_challenge prerequisite evaluation and type detection internals; although intended to be strictly additive, it alters text-matching behavior and could affect which gaps are surfaced in edge cases.

Overview
Bumps oddkit to 0.21.0 and documents the release in CHANGELOG.md.

Updates oddkit_challenge prerequisite checking to use parse-time stemmed token sets + runtime set intersection (with URL/numeric/proper-noun/citation structural tests and the non-trivial-input fallback preserved), and removes the module-level BM25 type-detection index cache in favor of rebuilding the index inline per request.

Adds a shared PrereqMatchVocab shape used by both base prerequisites and per-type prerequisite overlays, and expands the live smoke test to assert the new stemmed/structural prereq behavior and BM25 rebuild stability.

Reviewed by Cursor Bugbot for commit 33ca5bf. Bugbot is set up for automated code reviews on this repo. Configure here.

#120)

Closes the last two vodka anti-pattern remnants in oddkit_challenge per
P1.3.3, mirroring the matchers and the no-microsecond-caching discipline
gate shipped in 0.20.0.

Item 1 — D5 split-by-fit applied to challenge:
- evaluatePrerequisiteCheck migrated from regex-per-check to stemmed
  set intersection over PrereqMatchVocab (parsed once at canon-fetch).
- New parseCheckColumn helper extracts quoted vocabulary -> stemmedTokens
  Set and detects the four structural-test hints (URL, numeric,
  proper-noun, citation).
- BasePrerequisite + ChallengeTypeDef.prerequisiteOverlays both extended
  with PrereqMatchVocab via interface mixin.
- Runtime: tokenize(input) hoisted out of the per-prereq loop;
  per-prereq cost is now a Set lookup not a regex compile.
- Strictly additive: every input that matched the prior regex still
  matches; stemmed variations newly match; structural side-tests
  preserved verbatim from pre-refactor.

Item 2 — D9 applied to cachedChallengeTypeIndex:
- Module-level cachedChallengeTypeIndex + URL companion deleted.
- getOrBuildChallengeTypeIndex function deleted.
- cleanup_storage resets deleted.
- runChallengeAction call site rebuilds the BM25 type index inline per
  request (microsecond derivation; plumbing tax removed). Same pattern
  gate shipped in 0.20.0.

Item 3 — graduates new canon principle (separate canon PR, merged first):
- klappy://canon/principles/cache-fetches-and-parses live at klappy.dev
  3726073 (PR #125). Third deciding-argument recurrence satisfied.

Verification:
- typecheck clean
- governance-parser.test.mjs 105/105 pass
- ~9 new smoke assertions covering stemmed base + per-type matches,
  structural-test preservation (URL, proper-noun, citation), rebuild
  stability, and pre-refactor backward compat.
- Lockfile re-synced to 0.21.0 (was stale at 0.18.0 since 0.19.0 release).

PRD: /home/claude/work/prd-p1-3-3.md (working dir, not committed).
Handoff: klappy://odd/handoffs/2026-04-20-p1-3-3-challenge-revisit.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
oddkit 33ca5bf Commit Preview URL

Branch Preview URL
Apr 20 2026, 04:16 AM

@klappy klappy merged commit 25ad719 into prod Apr 20, 2026
5 checks passed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stop-word keywords silently dropped breaking additive invariant
    • Disabled stop-word filtering in both parseCheckColumn (vocabulary side) and inputStems (input side) by passing an empty Set to tokenize(), so canon keywords like "from" that happen to be stop words survive on both sides and participate in stemmed set intersection, restoring the strictly-additive invariant vs. the prior regex evaluator.
Preview (33ca5bfc98)
diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts
--- a/workers/src/orchestrate.ts
+++ b/workers/src/orchestrate.ts
@@ -2145,7 +2145,10 @@
   // the loop, stemmedTokens differ per prereq. Per PRD D3 (P1.3.3): stemmed
   // set intersection at runtime, structural tests preserved, no regex compile
   // per check. This is the fit-to-problem matcher per D5.
-  const inputStems = new Set(tokenize(input));
+  // Disable stop-word filtering to preserve canon keywords that happen to be
+  // stop words (e.g. `from` in source-named vocab). Must match the tokenize()
+  // call in parseCheckColumn so vocab and input stems share the same shape.
+  const inputStems = new Set(tokenize(input, new Set()));
   const missing: string[] = [];
   for (const p of prereqMap.values()) {
     const passed = evaluatePrerequisiteCheck(inputStems, input, p);
@@ -2323,10 +2326,16 @@
   let m: RegExpExecArray | null;
   while ((m = quotedRegex.exec(check)) !== null) {
     // Tokenize each quoted keyword or phrase — multi-word phrases like
-    // "according to" contribute multiple stems; stop-words are dropped
-    // by tokenize(). This preserves semantic coverage while normalizing
-    // morphology (problems → problem, considered → consid, etc.).
-    for (const stem of tokenize(m[1])) {
+    // "according to" contribute multiple stems. Disable stop-word filtering
+    // (empty Set) because canon vocab includes stop-word keywords — e.g.
+    // `from` in the source-named prereq, `by` / `as` / `with` elsewhere —
+    // and dropping them here would silently remove them from the matcher,
+    // breaking the strictly-additive invariant vs. the prior regex evaluator
+    // which tested each quoted keyword as a raw \b...\b regex against input.
+    // The input side (inputStems) uses the same empty stop-word set so the
+    // two sides share shape. Stemming still applies: morphology is normalized
+    // (problems → problem, considered → consid, etc.).
+    for (const stem of tokenize(m[1], new Set())) {
       stemmedTokens.add(stem);
     }
   }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 33ca5bf. Configure here.

// by tokenize(). This preserves semantic coverage while normalizing
// morphology (problems → problem, considered → consid, etc.).
for (const stem of tokenize(m[1])) {
stemmedTokens.add(stem);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stop-word keywords silently dropped breaking additive invariant

Medium Severity

parseCheckColumn calls tokenize(m[1]) on each quoted keyword, which applies the default STOP_WORDS filter. Quoted canon keywords that happen to be stop words — like "from" (explicitly listed in the comment at line 2351 as part of the source-named vocab) — are silently dropped from stemmedTokens. The same filtering on tokenize(input) at the call site removes them from inputStems too. The old regex evaluator matched "from" via new RegExp("\\bfrom\\b", "i") against the raw input, so an input like "I got this from my colleague" would pass source-named before but fails now. This breaks the "strictly additive" invariant claimed by the PR and CHANGELOG.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33ca5bf. Configure here.

klappy added a commit that referenced this pull request Apr 20, 2026
….21.1)

Two fixes from Cursor Bugbot findings on PR #120 / #121 that should have
blocked the 0.21.0 ship and didn't because the orchestrator treated
Bugbot's in_progress state as non-blocking. The findings are real and
the medium-severity one is a prod regression vs pre-refactor behavior.

Bug #1 (medium) — Stop-word filtering silently drops canon vocab keywords:
- parseCheckColumn called tokenize(m[1]) with default STOP_WORDS filter.
  Canon vocab includes 'from' (in source-named) and 'to' (inside
  'according to'); STOP_WORDS includes both. Result: 'from' is dropped
  from stemmedTokens; the runtime call site applied the same filter to
  inputStems so 'from' is dropped there too.
- Pre-refactor regex evaluator matched 'from' literally as
  new RegExp('\\bfrom\\b', 'i') against raw input. Inputs like
  'I learned this from my colleague' passed source-named pre-refactor
  and fail post-refactor — the strictly-additive invariant the 0.21.0
  CHANGELOG and PR description claimed is broken.
- Latent landmine: any prereq whose canon vocab is entirely stop-words
  would have stemmedTokens.size === 0 and trigger the conservative
  length>=20 fallback inappropriately (false positive). Not currently
  exploited by canon but the structural risk is there.
- Fix: pass new Set() (empty stop-words) to both tokenize() calls so
  canon keywords survive on both sides and shape matches.

Bug #2 (low) — DRY violation in BasePrerequisite:
- 0.21.0 introduced PrereqMatchVocab interface to share shape between
  BasePrerequisite and ChallengeTypeDef.prerequisiteOverlays[]. The
  inline type on prerequisiteOverlays uses '& PrereqMatchVocab'
  intersection; BasePrerequisite re-listed all five fields manually.
  Future PrereqMatchVocab additions would not propagate to
  BasePrerequisite — defeats the DRY purpose.
- Fix: split into BasePrerequisiteCore (3 core fields) and
  type BasePrerequisite = BasePrerequisiteCore & PrereqMatchVocab.

Verification:
- Typecheck clean
- governance-parser.test.mjs 105/105 pass
- 2 new regression assertions in canon-tool-envelope.smoke.mjs:
  (10) source-named via 'from'-only canon keyword
  (11) source-named via 'according to' multi-word phrase
- Bugbot Autofix preview diff for PR #121 used as the basis for the
  Bug #1 fix (cherry-picked the change locally rather than applying
  via Autofix UI to keep the audit trail in this branch).

Process post-mortem to follow in P1.3.3 closeout ledger.
klappy added a commit that referenced this pull request Apr 20, 2026
….21.1) (#122)

Two fixes from Cursor Bugbot findings on PR #120 / #121 that should have
blocked the 0.21.0 ship and didn't because the orchestrator treated
Bugbot's in_progress state as non-blocking. The findings are real and
the medium-severity one is a prod regression vs pre-refactor behavior.

Bug #1 (medium) — Stop-word filtering silently drops canon vocab keywords:
- parseCheckColumn called tokenize(m[1]) with default STOP_WORDS filter.
  Canon vocab includes 'from' (in source-named) and 'to' (inside
  'according to'); STOP_WORDS includes both. Result: 'from' is dropped
  from stemmedTokens; the runtime call site applied the same filter to
  inputStems so 'from' is dropped there too.
- Pre-refactor regex evaluator matched 'from' literally as
  new RegExp('\\bfrom\\b', 'i') against raw input. Inputs like
  'I learned this from my colleague' passed source-named pre-refactor
  and fail post-refactor — the strictly-additive invariant the 0.21.0
  CHANGELOG and PR description claimed is broken.
- Latent landmine: any prereq whose canon vocab is entirely stop-words
  would have stemmedTokens.size === 0 and trigger the conservative
  length>=20 fallback inappropriately (false positive). Not currently
  exploited by canon but the structural risk is there.
- Fix: pass new Set() (empty stop-words) to both tokenize() calls so
  canon keywords survive on both sides and shape matches.

Bug #2 (low) — DRY violation in BasePrerequisite:
- 0.21.0 introduced PrereqMatchVocab interface to share shape between
  BasePrerequisite and ChallengeTypeDef.prerequisiteOverlays[]. The
  inline type on prerequisiteOverlays uses '& PrereqMatchVocab'
  intersection; BasePrerequisite re-listed all five fields manually.
  Future PrereqMatchVocab additions would not propagate to
  BasePrerequisite — defeats the DRY purpose.
- Fix: split into BasePrerequisiteCore (3 core fields) and
  type BasePrerequisite = BasePrerequisiteCore & PrereqMatchVocab.

Verification:
- Typecheck clean
- governance-parser.test.mjs 105/105 pass
- 2 new regression assertions in canon-tool-envelope.smoke.mjs:
  (10) source-named via 'from'-only canon keyword
  (11) source-named via 'according to' multi-word phrase
- Bugbot Autofix preview diff for PR #121 used as the basis for the
  Bug #1 fix (cherry-picked the change locally rather than applying
  via Autofix UI to keep the audit trail in this branch).

Process post-mortem to follow in P1.3.3 closeout ledger.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant