Conversation
#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.
Deploying with
|
| 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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
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); |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 33ca5bf. Configure here.
….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.
….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.


Promotes oddkit 0.21.0 from main to prod.
What ships
oddkit_challengeD5 stemmed prereq matcher (Item 1, PR feat(challenge): D5 stemmed prereq matcher + D9 cache removal (0.21.0) #120)oddkit_challengeD9 cache removal (Item 2, PR feat(challenge): D5 stemmed prereq matcher + D9 cache removal (0.21.0) #120)klappy://canon/principles/cache-fetches-and-parses(klappy.dev#125, already live)Attestation
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
oddkit_encodeoddkit_challengeoddkit_gateChallenge has full canon parity with gate's matcher and caching discipline.
Refs
33ca5bf)3726073)Note
Medium Risk
Changes
oddkit_challengeprerequisite 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.0and documents the release inCHANGELOG.md.Updates
oddkit_challengeprerequisite 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
PrereqMatchVocabshape 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.