Skip to content

chore: promote 0.20.0 to prod#119

Merged
klappy merged 1 commit into
prodfrom
main
Apr 20, 2026
Merged

chore: promote 0.20.0 to prod#119
klappy merged 1 commit into
prodfrom
main

Conversation

@klappy
Copy link
Copy Markdown
Owner

@klappy klappy commented Apr 20, 2026

Promote 0.20.0 from main to prod.

What's shipping

PR #118 (P1.3.2 Phase 2) merged at 260492c. oddkit_gate now reads governance from canon at runtime:

  • klappy://odd/gate/transitions — transition keys, detection terms, prereq id mappings
  • klappy://odd/gate/prerequisites — prereq definitions, check vocabularies, gap messages

Envelope declares governance_source + governance_uris (plural array of 2) + debug.knowledge_base_url echo. Transition detection uses BM25 stemmed matching (ranking problem); prereq evaluation uses stemmed set intersection (independent gap-or-not; avoids BM25 IDF-negative pathology on small shared-vocabulary corpora).

Strictly additive — every input that matched 0.19.0's hardcoded word-boundary regex still matches, plus stemmed variations (deploying, started building, reconsidering etc.) now match too.

Verification

  • Preview branch smoke: 158/158 × 3 consecutive clean against https://gate-governance-source-envelope-oddkit.klappy.workers.dev
  • CI Test CF Preview: passed on PR feat(gate): governance-driven BM25 + set intersection + envelope (0.20.0) #118
  • Main preview smoke: 158/158 × 3 consecutive clean against https://main-oddkit.klappy.workers.dev (just ran)
  • Typecheck + governance-parser tests: clean (105/105)

Canon-first satisfied

Refs

Post-merge prod-smoke expected 158/158 × 3 consecutive after ~40s warmup.


Note

Medium Risk
Updates oddkit_gate’s core transition/prerequisite logic and response schema (new governance fields and changed prereq outputs), which may break consumers relying on prior matching behavior or string formats, though it includes explicit minimal fallbacks and added tests.

Overview
Promotes v0.20.0 (bumps root and worker versions) and documents the release in CHANGELOG.md, centered on a canon-driven refactor of oddkit_gate.

oddkit_gate now loads transition and prerequisite vocabulary from canon at runtime with a hardcoded minimal fallback, switches transition detection from regex cascades to BM25 + stemming, and changes prerequisite checks to stemmed set-intersection. The tool’s envelope now includes governance_source, governance_uris (array of 2), and echoes debug.knowledge_base_url.

The gate result contract changes: prerequisites.met now returns prereq ids (not descriptions) and prerequisites.unmet returns canon gap messages; the smoke test suite is extended to cover the new envelope fields, override behavior, BM25 priority resolution, and stemmed matching cases.

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

…0.0) (#118)

P1.3.2 Phase 2. runGateAction refactored to consume klappy://odd/gate/transitions
and klappy://odd/gate/prerequisites at runtime via two new helpers
(fetchGateTransitions, fetchGatePrerequisites). Transition detection via BM25
stemmed matching (ranking problem); prereq evaluation via stemmed set
intersection (independent gap-or-not, avoids BM25 IDF-negative pathology
on small shared-vocabulary corpora). Envelope declares governance_source
+ governance_uris (plural array of 2) + debug.knowledge_base_url echo.

Preview smoke 158/158 × 3 consecutive clean.

Canon-first satisfied: klappy/klappy.dev#120 + #122 merged before this PR.
@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 260492c Commit Preview URL

Branch Preview URL
Apr 20 2026, 03:16 AM

@klappy klappy merged commit 1308245 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 4 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Transition detection now includes context, changing behavior
    • Changed BM25 transition detection to use only input (not fullInput), preserving the old behavior of scoping transition keywords to the user's proposed transition phrase while still allowing fullInput for prereq stem matching.
  • ✅ Fixed: Unknown prereqs silently pass gate with inconsistent count
    • Gate now fails closed on governance errors: gateStatus is NOT_READY when either unmet or unknown prereqs exist, so unresolved prereq IDs no longer let the gate advance state with an inconsistent required_met/required_total count.
  • ✅ Fixed: Old detectTransition function is now dead code
    • Removed the unused detectTransition function from workers/src/orchestrate.ts; BM25-based detection in runGateAction is now the sole transition path.
  • ✅ Fixed: Prereq IDs not backtick-stripped unlike transition keys
    • Both the transitions-table prereq-IDs column and the prerequisites-table id column now strip backticks before splitting/trimming, so backtick-wrapped canon identifiers match rather than silently routing to unknown.
Preview (260492c6a2)
diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts
--- a/workers/src/orchestrate.ts
+++ b/workers/src/orchestrate.ts
@@ -348,26 +348,6 @@
   return { mode: sorted[0][0], confidence };
 }
 
-function detectTransition(input: string): { from: string; to: string } {
-  if (/\b(ready to build|ready to implement|start building|let's code|start coding)\b/i.test(input))
-    return { from: "planning", to: "execution" };
-  if (
-    /\b(ready to plan|start planning|let's plan|time to plan|move to planning|moving to planning)\b/i.test(
-      input,
-    )
-  )
-    return { from: "exploration", to: "planning" };
-  if (/\b(moving to execution|moving to build)\b/i.test(input))
-    return { from: "planning", to: "execution" };
-  if (/\b(back to exploration|need to rethink|step back|reconsider)\b/i.test(input))
-    return { from: "execution", to: "exploration" };
-  if (/\b(ship|deploy|release|go live|push to prod)\b/i.test(input))
-    return { from: "execution", to: "completion" };
-  if (/\b(ready|let's go|proceed|move forward|next step)\b/i.test(input))
-    return { from: "exploration", to: "planning" };
-  return { from: "unknown", to: "unknown" };
-}
-
 // Discover encoding types from canon governance docs.
 //
 // Governance resolution per canon/constraints/core-governance-baseline:
@@ -733,7 +713,7 @@
             const key = cols[0].replace(/`/g, "").trim();
             const from = cols[1].trim();
             const to = cols[2].trim();
-            const prereqIdsRaw = cols[3].trim();
+            const prereqIdsRaw = cols[3].replace(/`/g, "").trim();
             const detectionText = cols[4].trim();
             if (key.length === 0) continue;
             const prereqIds = prereqIdsRaw.length > 0
@@ -787,7 +767,7 @@
         for (const row of section[1].split("\n").filter((r: string) => r.includes("|"))) {
           const cols = parseTableRow(row);
           if (cols.length >= 3) {
-            const id = cols[0].trim();
+            const id = cols[0].replace(/`/g, "").trim();
             const check = cols[1].trim();
             const gapMessage = cols[2].replace(/^"|"$/g, "").trim();
             if (id.length === 0) continue;
@@ -2368,7 +2348,7 @@
   // deterministically when two transitions score identically.
   const bm25Docs = transitions.map((t) => ({ id: t.key, text: t.detectionText }));
   const transitionIndex = buildBM25Index(bm25Docs);
-  const hits = searchBM25(transitionIndex, fullInput, transitions.length);
+  const hits = searchBM25(transitionIndex, input, transitions.length);
 
   let matchedTransition: TransitionDef | null = null;
   if (hits.length > 0 && hits[0].score > 0) {
@@ -2414,7 +2394,7 @@
     }
   }
 
-  const gateStatus = unmet.length > 0 ? "NOT_READY" : "PASS";
+  const gateStatus = unmet.length > 0 || unknown.length > 0 ? "NOT_READY" : "PASS";
   const requiredTotal = matchedTransition ? matchedTransition.prereqIds.length : 0;
 
   // Update state

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

Reviewed by Cursor Bugbot for commit 260492c. Configure here.

// deterministically when two transitions score identically.
const bm25Docs = transitions.map((t) => ({ id: t.key, text: t.detectionText }));
const transitionIndex = buildBM25Index(bm25Docs);
const hits = searchBM25(transitionIndex, fullInput, transitions.length);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Transition detection now includes context, changing behavior

Medium Severity

BM25 transition detection uses fullInput (input + context), but the old detectTransition used only input. This means optional context (described as "what's been decided so far") can now influence which transition is detected. If a user provides vague input like "what next?" with context containing phrases like "we shipped the feature", the gate could detect execution-to-completion from context alone — the old code would have returned unknown/unknown. The PR claims "strictly additive" matching, but widening the detection input surface is a semantic change, not just a stemming addition.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 260492c. Configure here.

}

const gateStatus = unmet.length > 0 ? "NOT_READY" : "PASS";
const requiredTotal = matchedTransition ? matchedTransition.prereqIds.length : 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unknown prereqs silently pass gate with inconsistent count

Low Severity

When a transition references a prereq ID not found in the prerequisites map (governance configuration error), the prereq goes into unknown rather than unmet. Since gateStatus only checks unmet.length > 0, unknown prereqs don't block the gate. This creates a contradictory response: status is PASS but required_met < required_total (e.g., "2/3 required met" alongside "PASS"), and the gate advances state despite unresolved prerequisites.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 260492c. Configure here.

// deterministically when two transitions score identically.
const bm25Docs = transitions.map((t) => ({ id: t.key, text: t.detectionText }));
const transitionIndex = buildBM25Index(bm25Docs);
const hits = searchBM25(transitionIndex, fullInput, transitions.length);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Old detectTransition function is now dead code

Low Severity

The detectTransition function (line 351 in orchestrate.ts) is no longer called anywhere in the workers codebase. The only call site was in runGateAction, which this PR replaced with BM25-based detection. The function is now dead code that could confuse future readers about the intended transition detection path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 260492c. Configure here.

const detectionText = cols[4].trim();
if (key.length === 0) continue;
const prereqIds = prereqIdsRaw.length > 0
? prereqIdsRaw.split(",").map((s: string) => s.trim()).filter((s: string) => s.length > 0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prereq IDs not backtick-stripped unlike transition keys

Medium Severity

The transition key column strips backticks via .replace(//g, ""), but prereqIdsRawat the prereq-ID column does not. If the canon markdown table wraps prereq IDs in backticks (a natural formatting choice, consistent with how keys are formatted), the parsed IDs would retain backticks (e.g., ``decisions_locked``) and fail to match against the backtick-free IDs parsed from the prerequisites table. This would silently route every prereq tounknown`, and — per the separate gate-pass-on-unknown issue — the gate would pass with zero prereqs evaluated.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 260492c. Configure here.

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