Skip to content

promote: 0.26.0 to prod (oddkit_audit action — link-rot Phase 2)#146

Merged
klappy merged 8 commits intoprodfrom
main
Apr 27, 2026
Merged

promote: 0.26.0 to prod (oddkit_audit action — link-rot Phase 2)#146
klappy merged 8 commits intoprodfrom
main

Conversation

@klappy
Copy link
Copy Markdown
Owner

@klappy klappy commented Apr 27, 2026

Promotes oddkit_audit (PR #143) to production.

What ships

  • New MCP action oddkit_audit — mechanical detection of dead klappy:// references and legacy markdown link patterns. Per klappy://docs/oddkit/specs/oddkit-audit (DRAFT v2.1).
  • Two rule_ids: dead-reference (a klappy:// URI that doesn't resolve, including chains that end NOT_FOUND or cycle) and legacy-link-pattern (/page/... or ./*.md in writings/).
  • Both severity error by default.
  • Line-level allowlist via <!-- audit-allow: <rule-id> reason="..." --> directives.
  • Default scope: ["writings/"] only — the actual link-rot pain surface. Other paths are explicit opt-in via scope.paths.
  • Bounded by MAX_AUDIT_FILES=1000 and MAX_AUDIT_FINDINGS=500 with truncation flagged in summary.truncated.
  • Backward-compatible — purely additive net-new action.

Version

0.25.0 → 0.26.0

Validation receipts (Rule 1 — pre-merge)

PR #143 cleared every check before merging to main:

  • ✅ Workers Builds — success
  • ✅ Test CF Preview — success
  • ✅ Version Sync — success
  • ✅ Creed Freshness — success
  • ✅ Cursor Bugbot — completed/success after 3 review rounds (10 line-level findings across rounds, all dispositioned: 3 fix commits + 1 autofix landed on the branch before merge)

Bugbot finding disposition trail (commits on feat/oddkit-audit):

  • 875ff780 — round 1: suppression-1-line, supersession walker, findings cap, no-op ternary, reason field
  • 164e69d9 — round 2: out-of-scope expiration, depth-cap parity, dead uriExists, schema bridge type
  • 477bc2f2 — round 3 autofix: non-matching findings expiration

Validation receipts (Rule 2 — independent fresh-context validator)

Per klappy://canon/constraints/release-validation-gate Rule 2: this PR adds a new oddkit_* action and modifies workers/src/orchestrate.ts (+311 lines) — load-bearing surface. Independent Sonnet 4.6 read-only validator dispatched against PR #143 / SHA 6bc0595d and main preview URL before this promote merges. Findings + disposition will be appended below before merge.

Validator findings (Sonnet 4.6, fresh-context, read-only)

Dispatched against PR #143 / SHA 6bc0595d and main preview URL via Managed Agents API (agent_011CaTUhej22aHQ8yxuH5cKF, session sesn_011CaTUjLzivzqVVboKnfUjw). Full report at the agent's /home/user/ledger/rv-gate-pr146-v0.26.0-oddkit-audit.md.

ID Severity Finding Disposition
F-1 MEDIUM oddkit_audit tool description claimed default scope was writings/, canon/, odd/, docs/; actual DEFAULT_AUDIT_PATHS = ["writings/"] Fixed — PR #147 (squash 428b6769), merged to main pre-promote. New head SHA reflects fix.
F-2 LOW PR #143 body table stale post-merge Doc-debt, no live impact. No action.
F-3 MEDIUM Spec mandated index_state: {warm_count, warming_count}; shipped omits it Intentional drift — CHANGELOG documents. Spec amendment to v2.2 queued as canon follow-up PR on klappy.dev (precedent: spec was amended once at v2.1). Drop index_state from spec or mark as planned-future.
F-4 LOW suppressed_findings returns full objects; spec said count only Additive, not harmful. No action.
F-5 INFO PARTIAL_INDEX status absent Intentional; spec disagrees — folded into F-3 spec amendment.
F-6 INFO since_commit accepted but ignored Documented in CHANGELOG. No action.
F-7 env Validator's CF Workers container hit DNS overflow on POST /mcp Not a code defect — CI's "Test CF Preview" green on same surface (success on PR #146 head).

Verdict: fix-forward complete → cleared to merge. C2 (bytes-on-main) and C4 (canon retrievability) PASSED in validator report.

Cursor Bugbot findings on this PR

Bugbot ran on 428b6769 (post-PR #147 merge); parent check completed/neutral with one finding:

Path:Line Severity Finding Disposition
workers/src/orchestrate.ts:1947,1959 LOW Unused lineSeen property on pendingSuppress — assigned but never read (only .rule and .reason are read at L1976-1978). Genuine dead code. Waived for this promote, tracked at #148. Fix would re-touch orchestrate.ts (load-bearing per klappy://canon/constraints/release-validation-gate Rule 2) and force another full validator dispatch for a cosmetic cleanup. Will land in the next PR that legitimately touches audit code paths.

Cursor Bugbot Autofix may remain in_progress indefinitely without blocking per release-validation-gate Rule 1 — autofix runs after the parent review, which has already completed.

All findings dispositioned; cleared to merge.

Post-promotion verification

After CF auto-deploy completes (1–3 min), verify against https://oddkit.klappy.dev/mcp:

  1. oddkit_version returns 0.26.0
  2. The unified router accepts action: "audit" (12 enum values, not 11)
  3. oddkit_audit with default scope returns { status: "OK" | "FINDINGS", summary, findings, scope: { paths: ["writings/"] } }
  4. oddkit_audit with { scope: { paths: ["writings/", "canon/"] } } honors the wider scope

Refs

  • PR feat(audit): add oddkit_audit MCP action — Phase 2 PR-2.3a #143 (the implementation being promoted)
  • canon(constraints): bug-class lessons from resolver PR (Phase 2 PR-2.3b) klappy.dev#146 (sibling canon PR — three constraints + spec v2.1 amendment, merged)
  • Spec: klappy://docs/oddkit/specs/oddkit-audit (DRAFT v2.1)
  • Resolver dependency: klappy://docs/oddkit/specs/oddkit-resolve (in prod at v0.25.0)
  • Principle: klappy://canon/principles/identity-resolved-by-protocol
  • Constraint: klappy://canon/constraints/release-validation-gate (the Rule 2 obligation this validator dispatch satisfies)
  • Campaign: klappy://docs/planning/link-rot-elimination-campaign
  • Handoff carrying this work across sessions: klappy://odd/handoffs/2026-04-27-link-rot-phase-2-promote-and-phase-3 (note: handoff recommended deferring validator dispatch; canon Rule 3 overrides — validator dispatched anyway)

Note

Medium Risk
Medium risk because it introduces a new production-facing action that scans and fetches many markdown files and adds new routing/validation paths in the Worker. Existing actions are intended to remain unchanged, but performance and response-shape regressions are possible.

Overview
Ships oddkit_audit (v0.26.0): a new Worker action/tool (and unified-router action: "audit") that scans markdown within a configurable scope (default writings/) and returns structured findings for dead klappy:// references and legacy link patterns (/page/..., ./*.md in writings/), with a line-level <!-- audit-allow: ... --> suppression mechanism and caps/truncation reporting.

Updates the MCP surface to register audit everywhere it’s required (VALID_ACTIONS, unified oddkit enum, standalone oddkit_audit tool), normalizes non-string input args to JSON for this tool, adds Cloudflare production smoke tests for oddkit_audit, and bumps root + worker package versions/lockfiles to 0.26.0 with a corresponding CHANGELOG entry.

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

Klappy via Claude and others added 7 commits April 26, 2026 21:02
…link detection

Phase 2 PR-2.3 of the link-rot-elimination campaign. Implements
oddkit_audit per klappy://docs/oddkit/specs/oddkit-audit (DRAFT v2 — KISS).

Walks every markdown file in scope (writings/, canon/, odd/, docs/,
excluding docs/archive/). For each link target:
  - klappy:// URI: resolves through the index (with same shape-tolerance
    as oddkit_resolve for superseded_by chains). NOT_FOUND or circular
    → dead-reference error.
  - /page/... or ./*.md in writings/: legacy-link-pattern error.
  - everything else (external, anchors, valid non-klappy paths): ignored.

Line-level allowlist via <!-- audit-allow: <rule-id> reason="..." -->.
Suppressed findings returned in a separate envelope field so reviewers
can challenge the reason.

Bounded by MAX_AUDIT_FILES=1000 and MAX_AUDIT_FINDINGS=500 with truncation
flagged in summary.truncated. Production canon is ~560 docs; well below cap.

Three places updated for the new action surface (per
klappy://canon/constraints/oddkit-action-registration-completeness):
  - dispatch switch in handleUnifiedAction
  - VALID_ACTIONS array
  - central router enum + standalone tool definition

Two new smoke tests added:
  - 14j: default-scope audit returns OK or FINDINGS with valid summary
  - 14k: narrow-scope audit honors paths filter

Vodka discipline preserved: v1 of spec proposed four checks plus
supporting registries; v2 cut to one check, two rule_ids. Other checks
deferred per klappy://docs/planning/link-rot-deferred-concerns.

Version bump: 0.25.0 → 0.26.0

Refs:
- Spec: klappy://docs/oddkit/specs/oddkit-audit (DRAFT v2)
- Resolver: klappy://docs/oddkit/specs/oddkit-resolve (in prod v0.25.0)
- Principle: klappy://canon/principles/identity-resolved-by-protocol
- Bug-class lessons (separate canon PR in klappy/klappy.dev):
  klappy://canon/constraints/oddkit-action-registration-completeness
  klappy://canon/constraints/superseded-by-shape-normalization
  klappy://canon/constraints/bash-test-rig-assignment-chain-discipline
- Canon basis: klappy://canon/constraints/release-validation-gate,
  klappy://canon/principles/vodka-architecture
…, findings cap

- Remove no-op ternary in handleUnifiedAction audit dispatch
- Preserve audit-allow suppression across blank/prose lines until a link is seen
- Surface suppression reason on suppressed findings via suppression_reason field
- Match runResolve.lookupSuccessor normalization in uriResolves (.md stem fallback)
- Honor MAX_AUDIT_FINDINGS within per-line loop to enforce the 500-per-call cap
…chema bridge

- audit: suppression directives now expire only on finding-producing
  links, not on out-of-scope links classifyLink ignores.
- audit: depth-cap exhaustion in uriResolves now matches runResolve --
  treat as circular only when the last entry still declares a successor.
- audit: drop unreachable uriExists helper; uriResolves is only invoked
  with klappy:// URIs, so an absent index entry is a definitive miss.
- bridge: normalize object input to a JSON string before calling
  handleUnifiedAction so UnifiedParams.input: string holds at runtime
  for oddkit_audit's union schema.
…surface

CF Preview test 14j (default-scope audit) timed out at 120s on the prior
default of [writings/, canon/, odd/, docs/]. Cold-cache fetching ~560
files through the worker's zip-extract path exceeded the curl budget.

v1 default scope is writings/ only. Reasons it's honest, not a hack:
- PR-2.2's actual cleanup was writings-only; the campaign motivation
  was reader complaints about broken links in published essays.
- April-9 reference-integrity audit classified the 49 unfixed refs as
  intentional (template placeholders, site routes, historical archive,
  .cursor/plans) — none in writings/.
- writings/ is where authors write klappy:// URIs as body links most
  often; canon/odd/docs use frontmatter cross-refs which the resolver
  governs separately.

canon/, odd/, docs/ become explicit opt-in via scope.paths. Reversal
is one line if a real consumer demonstrates wider need (or if
parallelized fetching graduates from the deferred-concerns ledger).

Spec amendment to klappy://docs/oddkit/specs/oddkit-audit (v2.1) lands
in the sibling canon PR (klappy/klappy.dev#146) so the spec self-
documents the deviation rather than the code silently diverging.

Refs:
- klappy://docs/oddkit/specs/oddkit-audit (DRAFT v2.1 — to be amended)
- klappy://docs/planning/link-rot-deferred-concerns (parallelized
  fetching is a candidate for the deferred ledger)
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 27, 2026

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 428b676 Commit Preview URL

Branch Preview URL
Apr 27 2026, 02:36 AM

klappy added a commit that referenced this pull request Apr 27, 2026
Validator F-1 (RV-gate dispatch on PR #146): tool description claimed default scope was writings/, canon/, odd/, docs/ but actual DEFAULT_AUDIT_PATHS is writings/ only. Misleads every MCP consumer reading the schema. Description-only fix; no behavior change.

Also: spec version reference DRAFT v2 -> DRAFT v2.1 (klappy.dev#146 amendment).
…ate F-1) (#147)

Validator F-1 (RV-gate dispatch on PR #146): tool description claimed default scope was writings/, canon/, odd/, docs/ but actual DEFAULT_AUDIT_PATHS is writings/ only. Misleads every MCP consumer reading the schema. Description-only fix; no behavior change.

Also: spec version reference DRAFT v2 -> DRAFT v2.1 (klappy.dev#146 amendment).
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: Unused lineSeen property is dead code
    • Removed the unused lineSeen field from both the pendingSuppress type and its assignment site, and verified the worker still typechecks cleanly.
Preview (428b6769ea)
diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts
--- a/workers/src/orchestrate.ts
+++ b/workers/src/orchestrate.ts
@@ -1944,7 +1944,7 @@
     const lines = content.split("\n");
     // Track allowlist directives: when one appears, it suppresses the next
     // finding of the matching rule_id on the *next* link (any subsequent line).
-    let pendingSuppress: { rule: string; reason: string | null; lineSeen: number } | null = null;
+    let pendingSuppress: { rule: string; reason: string | null } | null = null;
 
     for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
       if (truncated) break;
@@ -1956,7 +1956,6 @@
         pendingSuppress = {
           rule: allowMatch[1],
           reason: allowMatch[2] || null,
-          lineSeen: lineIdx + 1,
         };
         // Don't continue — allowlist directives may sit on a line that also
         // contains a link they are NOT meant to suppress (rare, but possible).

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

Reviewed by Cursor Bugbot for commit 428b676. Configure here.

pendingSuppress = {
rule: allowMatch[1],
reason: allowMatch[2] || null,
lineSeen: lineIdx + 1,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused lineSeen property is dead code

Low Severity

The lineSeen property on the pendingSuppress object is assigned at creation but never read. Only pendingSuppress.rule and pendingSuppress.reason are accessed (at lines 1976–1978). The lineSeen field in the type definition and its assignment are dead code that can be removed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 428b676. 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.

2 participants