Skip to content

fix(challenge): restore strictly-additive invariant + DRY collapse (0.21.1) — addresses Bugbot findings on #120/#121#122

Merged
klappy merged 1 commit intomainfrom
fix/0.21.1-bugbot-stopword-and-dry
Apr 20, 2026
Merged

fix(challenge): restore strictly-additive invariant + DRY collapse (0.21.1) — addresses Bugbot findings on #120/#121#122
klappy merged 1 commit intomainfrom
fix/0.21.1-bugbot-stopword-and-dry

Conversation

@klappy
Copy link
Copy Markdown
Owner

@klappy klappy commented Apr 20, 2026

Fix-forward for two Cursor Bugbot findings the P1.3.3 orchestrator shipped past on 0.21.0.

The orchestrator merged PR #120 and PR #121 while Bugbot was still in_progress, treating it as non-blocking. Bugbot subsequently posted two findings on commit 400eb57b:

# Severity Finding
1 Medium Stop-word filtering silently drops quoted canon keywords — from (in source-named vocab) is a stop word in STOP_WORDS; default tokenize() strips it from both stemmedTokens and inputStems. Pre-refactor \bfrom\b regex matched literally; post-refactor it doesn't match at all. Strictly-additive invariant broken in prod.
2 Low BasePrerequisite re-declared the 5 PrereqMatchVocab fields manually instead of & PrereqMatchVocab intersection — defeats the DRY purpose of introducing the shared interface.

Both fixes verified live in prod 0.20.0 (the bug exists), prod 0.21.0 (still exists), and 0.21.1 preview (fixed).

What changed

Bug #1 — Stop-word filter disabled on canon vocab and input

File: workers/src/orchestrate.ts

  • parseCheckColumn: tokenize(m[1])tokenize(m[1], new Set()). Canon vocab includes from (source-named), to (inside according to), and any other stop-word that appears in quoted vocab survives. Doc comment updated to explain why.
  • Runtime call site at runChallengeAction: tokenize(input)tokenize(input, new Set()). Same empty stop-word set so input and vocab share shape. Doc comment updated.

This restores the strictly-additive invariant that 0.21.0's CHANGELOG and PR description claimed but did not actually deliver.

Bug #2BasePrerequisite collapsed to intersection

File: workers/src/orchestrate.ts

// Before (0.21.0):
interface BasePrerequisite {
  prerequisite: string;
  check: string;
  gapMessage: string;
  stemmedTokens: Set<string>;
  hasURLCheck: boolean;
  hasNumericCheck: boolean;
  hasProperNounCheck: boolean;
  hasCitationCheck: boolean;
}

// After (0.21.1):
interface BasePrerequisiteCore {
  prerequisite: string;
  check: string;
  gapMessage: string;
}
type BasePrerequisite = BasePrerequisiteCore & PrereqMatchVocab;

Future field additions to PrereqMatchVocab now propagate automatically.

New regression smoke assertions

Added 2 assertions to workers/test/canon-tool-envelope.smoke.mjs:

  • (10) from-only source attribution: "I learned this morning that the deploy regressed from my colleague Alex Brown — observed during last night's incident review." must NOT have "no source named" in missing_prerequisites. This is the regression anchor.
  • (11) according to multi-word phrase: "We saw a 30% latency regression according to the Tuesday measurements I observed in the dashboard." must NOT have "no source named". Documents the multi-word case where stop-word filtering would have dropped half the phrase.

Verification

Check Result
typecheck (workers) clean
governance-parser.test.mjs 105/105 pass
Preview smoke (fix-0-21-1-bugbot-stopword-and-dry-oddkit) × 3 consecutive 172/172 × 3
0.21.0 prod regression confirmed via curl from-only input fails source-named ✓ (bug exists)
0.21.1 preview regression-fixed via curl from-only input passes source-named ✓ (bug fixed)
Cursor Bugbot on this PR WAITING — will not merge until completed
Sonnet 4.6 read-only validator (Managed Agents) DISPATCHED — will not promote until findings folded into ledger

Process change context

This PR is also the first application of klappy://canon/constraints/release-validation-gate (currently in PR klappy/klappy.dev#126, open at time of this PR's open).

Per the new canon constraint:

  • Rule 1: This PR will not merge until Cursor Bugbot reaches completed.
  • Rule 2: The promotion PR for 0.21.1 will not merge until the Sonnet 4.6 read-only validator session completes and its findings are folded into the closeout ledger.
  • Rule 3: No session-scoped recommendation overrides these rules.

The full P1.3.3 process post-mortem and the structural fix (canon docs release-validation-gate + contract-governs-handoff-drift + bootstrap hook) are in klappy/klappy.dev#126.

Refs


Note

Medium Risk
Changes oddkit_challenge prerequisite tokenization to disable stop-word filtering, which can broaden or alter match behavior for user inputs and affect gating/messaging. Coverage is improved via new smoke-test regressions, but it still touches core evaluator logic.

Overview
Restores the strictly-additive prerequisite-matching behavior in oddkit_challenge by disabling stop-word filtering for both parse-time prereq vocab (parseCheckColumn) and runtime input stems, ensuring canon keywords like from are not silently dropped.

Refactors the shared prereq types so BasePrerequisite is defined as BasePrerequisiteCore & PrereqMatchVocab to keep the shared match-vocabulary shape DRY.

Adds smoke-test regressions covering stop-word keyword matching (from-only source attribution) and a multi-word phrase case (according to), and bumps versions/docs for the 0.21.1 release.

Reviewed by Cursor Bugbot for commit 9d09fe4. Bugbot is set up for automated code reviews on this repo. 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.
@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 9d09fe4 Commit Preview URL

Branch Preview URL
Apr 20 2026, 04:38 AM

@klappy
Copy link
Copy Markdown
Owner Author

klappy commented Apr 20, 2026

Release Validation Gate — Rule 1 Disposition

Per klappy://canon/constraints/release-validation-gate Rule 1:

Check Status Conclusion Disposition
Cursor Bugbot completed success Clean — no findings on commit 9d09fe4
Test CF Preview completed success Pass
Workers Builds: oddkit completed success Pass
Version Sync completed success Pass
Creed Freshness completed success Pass

All reviews completed before merge. Rule 1 satisfied.

Sonnet 4.6 read-only validator session sesn_011CaERPjHi1CV4TrvKW68jB (agent agent_011CaERLniErdgxJagniRtNP) was dispatched against this branch + 0.21.0 prod state + canon PR klappy/klappy.dev#126. Verdict: CONDITIONAL PASS. Conditional only on klappy/klappy.dev#126 merging first (which it has, as ee9aee4). All 5 corroborations either PASS or PARTIAL with documented reason. Validator findings folded into the upcoming closeout ledger.

Rule 2 (independent validator dispatch before promotion) is satisfied via the same session — the validator was explicitly tasked with reviewing both this PR and the upcoming promotion.

@klappy klappy merged commit d17bc0c into main Apr 20, 2026
5 checks passed
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