Conversation
…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)
Deploying with
|
| 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 |
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).
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: Unused
lineSeenproperty is dead code- Removed the unused
lineSeenfield from both thependingSuppresstype and its assignment site, and verified the worker still typechecks cleanly.
- Removed the unused
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, |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 428b676. Configure here.


Promotes
oddkit_audit(PR #143) to production.What ships
oddkit_audit— mechanical detection of deadklappy://references and legacy markdown link patterns. Perklappy://docs/oddkit/specs/oddkit-audit(DRAFT v2.1).rule_ids:dead-reference(aklappy://URI that doesn't resolve, including chains that end NOT_FOUND or cycle) andlegacy-link-pattern(/page/...or./*.mdinwritings/).errorby default.<!-- audit-allow: <rule-id> reason="..." -->directives.["writings/"]only — the actual link-rot pain surface. Other paths are explicit opt-in viascope.paths.MAX_AUDIT_FILES=1000andMAX_AUDIT_FINDINGS=500with truncation flagged insummary.truncated.Version
0.25.0 → 0.26.0
Validation receipts (Rule 1 — pre-merge)
PR #143 cleared every check before merging to main:
Bugbot finding disposition trail (commits on
feat/oddkit-audit):875ff780— round 1: suppression-1-line, supersession walker, findings cap, no-op ternary, reason field164e69d9— round 2: out-of-scope expiration, depth-cap parity, deaduriExists, schema bridge type477bc2f2— round 3 autofix: non-matching findings expirationValidation receipts (Rule 2 — independent fresh-context validator)
Per
klappy://canon/constraints/release-validation-gateRule 2: this PR adds a newoddkit_*action and modifiesworkers/src/orchestrate.ts(+311 lines) — load-bearing surface. Independent Sonnet 4.6 read-only validator dispatched against PR #143 / SHA6bc0595dand 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
6bc0595dand main preview URL via Managed Agents API (agent_011CaTUhej22aHQ8yxuH5cKF, sessionsesn_011CaTUjLzivzqVVboKnfUjw). Full report at the agent's/home/user/ledger/rv-gate-pr146-v0.26.0-oddkit-audit.md.oddkit_audittool description claimed default scope waswritings/, canon/, odd/, docs/; actualDEFAULT_AUDIT_PATHS = ["writings/"]428b6769), merged to main pre-promote. New head SHA reflects fix.index_state: {warm_count, warming_count}; shipped omits itindex_statefrom spec or mark as planned-future.suppressed_findingsreturns full objects; spec said count onlyPARTIAL_INDEXstatus absentsince_commitaccepted but ignored/mcpVerdict: 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 checkcompleted/neutralwith one finding:workers/src/orchestrate.ts:1947,1959lineSeenproperty onpendingSuppress— assigned but never read (only.ruleand.reasonare read at L1976-1978). Genuine dead code.orchestrate.ts(load-bearing perklappy://canon/constraints/release-validation-gateRule 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 Autofixmay remainin_progressindefinitely without blocking per release-validation-gate Rule 1 — autofix runs after the parent review, which has already completed.Post-promotion verification
After CF auto-deploy completes (1–3 min), verify against
https://oddkit.klappy.dev/mcp:oddkit_versionreturns0.26.0action: "audit"(12 enum values, not 11)oddkit_auditwith default scope returns{ status: "OK" | "FINDINGS", summary, findings, scope: { paths: ["writings/"] } }oddkit_auditwith{ scope: { paths: ["writings/", "canon/"] } }honors the wider scopeRefs
klappy://docs/oddkit/specs/oddkit-audit(DRAFT v2.1)klappy://docs/oddkit/specs/oddkit-resolve(in prod at v0.25.0)klappy://canon/principles/identity-resolved-by-protocolklappy://canon/constraints/release-validation-gate(the Rule 2 obligation this validator dispatch satisfies)klappy://docs/planning/link-rot-elimination-campaignklappy://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-routeraction: "audit") that scans markdown within a configurable scope (defaultwritings/) and returns structured findings for deadklappy://references and legacy link patterns (/page/...,./*.mdinwritings/), with a line-level<!-- audit-allow: ... -->suppression mechanism and caps/truncation reporting.Updates the MCP surface to register
auditeverywhere it’s required (VALID_ACTIONS, unifiedoddkitenum, standaloneoddkit_audittool), normalizes non-stringinputargs to JSON for this tool, adds Cloudflare production smoke tests foroddkit_audit, and bumps root + worker package versions/lockfiles to0.26.0with a corresponding CHANGELOG entry.Reviewed by Cursor Bugbot for commit 428b676. Bugbot is set up for automated code reviews on this repo. Configure here.