From c36941c5b28d346b4f919dd4366aeb240c437bd2 Mon Sep 17 00:00:00 2001 From: Chris Klapp Date: Sat, 25 Apr 2026 23:09:42 -0400 Subject: [PATCH 1/5] add docs/oddkit/specs/oddkit-resolve.md --- docs/oddkit/specs/oddkit-resolve.md | 147 ++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/oddkit/specs/oddkit-resolve.md diff --git a/docs/oddkit/specs/oddkit-resolve.md b/docs/oddkit/specs/oddkit-resolve.md new file mode 100644 index 00000000..a930e1ca --- /dev/null +++ b/docs/oddkit/specs/oddkit-resolve.md @@ -0,0 +1,147 @@ +--- +uri: klappy://docs/oddkit/specs/oddkit-resolve +title: "oddkit_resolve — Action Specification (DRAFT v4 — KISS)" +audience: docs +exposure: nav +tier: 2 +voice: neutral +stability: draft +tags: ["spec", "oddkit", "resolve", "supersession", "anti-cache-lying", "vodka", "kiss"] +epoch: E0008 +date: 2026-04-26 +derives_from: + - "canon/methods/supersession.md" + - "canon/principles/partial-data-with-transparency-and-background-warm.md" + - "odd/constraint/anti-cache-lying.md" + - "docs/oddkit/IMPL-catalog-recent.md" +governs: "Resolution of klappy:// URIs by every consumer of canon" +supersedes: "DRAFT v3 (2026-04-24, pre-Vodka-cut)" +--- + +# oddkit_resolve — Action Specification (DRAFT v4 — KISS) + +> Take a `klappy://` URI. Return the current canonical answer. Walk supersession transparently. That's the entire job. Everything else was bloat. + +## Conviction Shape + +- **High conviction**: resolution belongs to the protocol, not consumers; transparent supersession redirect; partial-data compliance is mandatory. +- **Working belief**: returning the supersession chain in metadata so consumers that want it can read it; defaulting to `replace`-style redirect when `superseded_by` is set in frontmatter without further qualification. +- **Tunable**: error-mode names; partial-data threshold defaults. + +## What This Does + +Input: a `klappy://` URI. +Output: the current canonical doc the URI points to, after walking any `superseded_by` chain in existing frontmatter. + +Nothing else. No meaning queries (deferred — see deferred-concerns ledger). No batch action (deferred). No `resolve_links` flag (deferred). No new frontmatter fields (deferred). One thin action over the index oddkit already maintains. + +## Why This Is Enough + +The failure mode reported was: hardcoded URLs and paths in markdown rot when files move or get superseded. The mechanism that fixes that failure is one capability — convert URI to current location at request time. Every other feature I drafted was building infrastructure for problems we haven't observed yet. Vodka says: don't. + +When a consumer hits a real problem the v1 resolver doesn't solve (e.g., needs meaning fallback because authors typo URIs constantly, or needs batch resolution because a build pipeline is making N round-trips and choking), that pain becomes the trigger for adding the next capability — not before. + +## The Action + +### Input + +```json +{ "uri": "klappy://writings/the-most-expensive-problem" } +``` + +That's it. One field. Required. + +### Output + +```json +{ + "action": "resolve", + "result": { + "status": "FOUND" | "NOT_FOUND" | "PARTIAL_INDEX", + "resolved": { + "uri": "klappy://writings/the-most-expensive-problem", + "path": "writings/the-most-expensive-problem.md", + "title": "The Most Expensive Problem", + "url": "/writings/the-most-expensive-problem" + }, + "supersession_chain": [ + { "uri": "klappy://writings/old-slug", "superseded_at": "2026-04-09" } + ], + "index_state": { + "warm_count": 552, + "warming_count": 0 + } + }, + "server_time": "2026-04-26T02:50:00.000Z" +} +``` + +- `resolved` populated when status is `FOUND` (or `PARTIAL_INDEX` with cache hit). Always the canonical terminus after walking supersession. +- `supersession_chain` populated only when supersession occurred. Ordered oldest → newest. Empty array (or omitted) when the input URI is the terminus. +- `index_state` mandatory per partial-data principle. Concrete numbers, not "data may be incomplete." + +### Algorithm + +1. Look up `uri` directly in the index. +2. If found and `superseded_by` is unset → `FOUND`, no chain. +3. If found and `superseded_by` is set → walk the chain to terminus, return terminus, populate `supersession_chain`. Transparent redirect. +4. If not found → `NOT_FOUND`. No candidates, no fallback, no fuzziness — that's deferred. +5. If the index is partially warm and the URI isn't in cache → `PARTIAL_INDEX` with `resolved` omitted and `index_state` showing the warming gap. Caller may retry. + +### Error modes + +| Status | Meaning | +|--------|---------| +| `FOUND` | Single canonical answer, possibly via supersession | +| `NOT_FOUND` | URI doesn't exist in the index | +| `PARTIAL_INDEX` | Index not fully warm; this answer is best-effort | +| `INVALID_INPUT` | Malformed call | +| `CIRCULAR_SUPERSESSION` | Index data error (`superseded_by` cycles) | + +## Partial-Data Compliance + +Per `klappy://canon/principles/partial-data-with-transparency-and-background-warm`: + +1. User-blocking path bounded by cache lookups, not corpus scan. +2. Background warm via `ctx.waitUntil`. Next request reads warm cache. +3. Concrete disclosure via `index_state`. + +Non-negotiable. Same shape as the existing `oddkit_search` and `oddkit_catalog` already implement. + +## Disconfirmers — What Would Falsify This + +1. **A consumer for whom request-time MCP round-trips are unacceptable AND no static-build-time alternative exists.** This was the original reason I drafted `oddkit_resolve_batch`. v1 doesn't ship it; if Lovable or another consumer demonstrates this pain in real use, the deferred batch action graduates from the deferred ledger. +2. **Authors typo URIs at a rate that makes pure URI-resolution insufficient.** Original argument for meaning queries. v1 punts; if the dead-reference CI gate (separate spec) reveals high typo rates, meaning fallback graduates. +3. **The five-response supersession taxonomy proves to need richer treatment than transparent redirect.** v1 treats every `superseded_by` as `replace`. If real cases of `graduate` / `tolerate` / `observe` produce broken behavior in consumers, the per-response field graduates. + +The principle survives all three falsifiers. They're triggers for extending the resolver, not retracting it. + +## What This Costs Us If We Don't Ship + +Every recurrence of the broken-link bug class on every consumer, forever. The reader complaints that started this campaign cannot be answered durably without protocol-level resolution. + +## Backward Compatibility + +Net-new action. No existing callers. No breaking change. + +## Migration + +1. Land this spec as committed canon. +2. Implement `oddkit_resolve` per the algorithm above. Promotion gated on independent Sonnet 4.6 validator pass per E0008.3 / `klappy://canon/constraints/release-validation-gate`. Validator verifies: direct hit, supersession chain, NOT_FOUND, partial-index handling. +3. Convert writings: every `[label](/page/...)` and `[label](./relative.md)` becomes a `klappy://` URI. Re-scan against `klappy/klappy.dev@main` `writings/*.md` immediately before opening the cleanup PR. + +## Open Questions (Tune During Build) + +1. Should the `index_state.stale_threshold_seconds` default come from the existing index infrastructure or be set per-action? Recommendation: read from index infrastructure for consistency with `oddkit_search`. +2. Should `CIRCULAR_SUPERSESSION` halt resolution or fall through to `NOT_FOUND`? Recommendation: halt with the explicit error so the canon bug gets fixed, not papered over. + +## See Also + +- [Supersession](klappy://canon/methods/supersession) — the chain semantics this resolver follows +- [Partial Data With Transparency And Background Warm](klappy://canon/principles/partial-data-with-transparency-and-background-warm) — partial-data compliance +- [Anti-Cache Lying](klappy://odd/constraint/anti-cache-lying) — the axiom this resolver applies to references +- [Deferred Concerns Ledger](klappy://docs/planning/link-rot-deferred-concerns) — what we cut and when to revisit + +## Origin + +Drafted on 2026-04-26 in response to recurring broken-link reports on klappy.dev. v1 proposed a CI gate against hardcoded markdown patterns. v2 added a meaning-query primitive, batch action, resolve_links flag, supersession-response field, aliases field, build-time companion, and a four-check audit. v3 incorporated canon-tier-2 challenge findings. **v4 (this revision)** applied the Vodka discipline the operator surfaced — every piece that was building for hypothetical pain got cut. Result is one action with one input, doing one job: convert URI to current location. Everything else moved to the deferred-concerns ledger with explicit revisit conditions. From 826a857675289498619dfa99ef645289c0eb8d87 Mon Sep 17 00:00:00 2001 From: Chris Klapp Date: Sat, 25 Apr 2026 23:09:43 -0400 Subject: [PATCH 2/5] add docs/oddkit/specs/oddkit-audit.md --- docs/oddkit/specs/oddkit-audit.md | 178 ++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/oddkit/specs/oddkit-audit.md diff --git a/docs/oddkit/specs/oddkit-audit.md b/docs/oddkit/specs/oddkit-audit.md new file mode 100644 index 00000000..03f6f505 --- /dev/null +++ b/docs/oddkit/specs/oddkit-audit.md @@ -0,0 +1,178 @@ +--- +uri: klappy://docs/oddkit/specs/oddkit-audit +title: "oddkit_audit — Action Specification (DRAFT v2 — KISS)" +audience: docs +exposure: nav +tier: 2 +voice: neutral +stability: draft +tags: ["spec", "oddkit", "audit", "dead-references", "ci-gate", "vodka", "kiss"] +epoch: E0008 +date: 2026-04-26 +derives_from: + - "canon/methods/reference-integrity-audit.md" + - "canon/principles/partial-data-with-transparency-and-background-warm.md" + - "canon/principles/ritual-is-a-smell.md" + - "docs/oddkit/specs/oddkit-resolve.md" +governs: "Mechanical detection of dead klappy:// references at PR time" +supersedes: "DRAFT v1 (2026-04-26, four-check version)" +--- + +# oddkit_audit — Action Specification (DRAFT v2 — KISS) + +> Walk every `klappy://` URI in canon. Call `oddkit_resolve` on each. Report the ones that don't resolve. That's the entire job. + +## Conviction Shape + +- **High conviction**: dead-reference detection is the load-bearing check; the audit reports, the workflow decides whether to fail; partial-data compliance is mandatory. +- **Working belief**: severity classification (error vs warning); allowlist via line-level directives only; legacy-link-pattern detection bundled in (different rule, same surface). +- **Tunable**: severity defaults; PR comment aggregation; how aggressively to flag `legacy-link-pattern` in unchanged files. + +## What This Does + +Input: a scope (paths + optional `since_commit`). +Output: structured findings for `klappy://` URIs that don't resolve, plus markdown link patterns we know are bad (`/page/...`, `./relative.md` in writings). + +Nothing else. No terminological-drift check. No projection-staleness check. No epoch-gap check. No deprecated-terms registry. No epoch-completeness rules. No `audit_allow:` frontmatter field. Each of those was building for a problem we haven't been asked to solve. They live in the deferred-concerns ledger with explicit revisit conditions. + +## Why This Is Enough + +The reported pain is broken links. The mechanism that prevents broken links is calling the resolver on every URI before merge. That's one check. Bundling four checks into one action conflated four problems; cutting back to one check makes each future addition a separate decision triggered by its own pain. + +When terminological drift, projection staleness, or epoch gaps cause concrete pain, each becomes its own thin action — not bolted into this one. + +## The Action + +### Input + +```json +{ + "action": "audit", + "input": { + "scope": { + "paths": ["writings/", "canon/", "odd/", "docs/"], + "since_commit": "main~1" + } + } +} +``` + +- `scope.paths` — repo-relative path prefixes. Default: full repo excluding `docs/archive/`. +- `scope.since_commit` — limit findings to files changed since this ref. Default: full audit. PR-mode CI sets this to merge-base. + +No `checks` field. There's one check; it always runs. No `severity_floor`. Workflow decides what to fail on. + +### Output + +```json +{ + "action": "audit", + "result": { + "status": "OK" | "FINDINGS" | "PARTIAL_INDEX", + "summary": { + "total_findings": 12, + "by_severity": { "error": 11, "warning": 1 } + }, + "findings": [ + { + "rule_id": "dead-reference", + "severity": "error", + "location": { "path": "writings/from-passive-to-proactive.md", "line": 47 }, + "occurrence": "klappy://writings/some-broken-slug", + "message": "URI does not resolve" + }, + { + "rule_id": "legacy-link-pattern", + "severity": "error", + "location": { "path": "writings/from-passive-to-proactive.md", "line": 53 }, + "occurrence": "/page/writings/some-slug", + "message": "Use a klappy:// URI instead of /page/ path" + } + ], + "index_state": { + "warm_count": 552, + "warming_count": 0 + } + }, + "server_time": "2026-04-26T02:50:00.000Z" +} +``` + +### Two rule_ids + +`dead-reference` (severity: `error`) — a `klappy://` URI that returns `NOT_FOUND` from `oddkit_resolve`. + +`legacy-link-pattern` (severity: `error`) — `[label](/page/...)` or `[label](./relative.md)` in `writings/`. These are the patterns that caused the original reader complaints; banning them at the lint level forces use of `klappy://` URIs which the resolver protects. + +That's the entire rule set. Other concerns are deferred. + +### Algorithm + +For every markdown file in scope: + +1. Extract every `[label](target)` markdown link. +2. For targets starting with `klappy://`: call `oddkit_resolve`. On `NOT_FOUND` → `dead-reference` finding. On `CIRCULAR_SUPERSESSION` → also `dead-reference` finding (the URI is functionally dead from the consumer's perspective). +3. For targets matching `/page/...` or `./*.md` in `writings/*.md`: emit `legacy-link-pattern` finding. + +Other targets (external URLs, anchors, valid relative paths outside writings): ignore. Not this action's job. + +### Allowlist + +One mechanism, line-level only: + +```markdown + +[some link](klappy://writings/not-yet-published) +``` + +Scoped to the next markdown link. One rule_id per directive. Suppressed findings appear in the audit envelope under `suppressed_findings` (count only, not in `summary` totals) so reviewers can see what was suppressed and challenge the reason if needed. + +No frontmatter `audit_allow:` field. Adding one is bloat for a problem we haven't observed. + +## Partial-Data Compliance + +Per the partial-data principle: + +1. User-blocking path bounded by cache lookups. +2. Background warm via `ctx.waitUntil`. +3. Concrete disclosure via `index_state`. + +When `status: PARTIAL_INDEX`, findings are best-effort. CI workflow handles this by treating partial-index runs as non-blocking (warning, retry on next push). + +## Disconfirmers — What Would Falsify This + +1. **The resolver has bugs that produce false `NOT_FOUND` responses.** Audit findings would be false positives. Mitigation: workflow respects `index_state.warming_count`; release-validation-gate on the resolver catches this before audit ships. +2. **Findings volume on first run is so high authors disable the gate.** Mitigation: workflow ships in soft-block mode; one observation cycle to assess before hard-block. +3. **The line-level allowlist proves insufficient (e.g., a template file legitimately has 30 placeholder URIs).** Triggers reconsideration of file-level allowlist (the deferred frontmatter field). + +## What This Costs Us If We Don't Ship + +The resolver alone fixes the consumer side. Without the audit, authoring discipline is the load-bearing layer for keeping URIs correct — and that's exactly what failed. The principle the resolver embodies stays unenforced at the source. + +## Backward Compatibility + +Net-new action. No existing callers. + +## Migration + +1. Land this spec as committed canon. +2. Implement `oddkit_audit` per the algorithm above. Promotion gated on independent Sonnet 4.6 validator pass per E0008.3 / `klappy://canon/constraints/release-validation-gate`. Validator verifies: real `NOT_FOUND` → error finding; real `FOUND` → no finding; legacy pattern in writings → error finding; line-level allowlist suppresses correctly; partial-index emits the right status. +3. Wire into `.github/workflows/canon-quality.yml` (separate artifact). Soft-block this cycle, escalate after observation. + +## Open Questions (Tune During Build) + +1. PR comment aggregation when findings volume is high. Recommendation: group by file with `
` collapse, cap at 50 findings rendered, link to full audit-response.json artifact. +2. Pre-commit hook performance — calling the live worker on every commit is too slow. Recommendation: defer pre-commit to a follow-up; CI-only enforcement is sufficient for v1. +3. Severity for `legacy-link-pattern` in unchanged files. Recommendation: only emit on files modified in the PR; ignore for unchanged files (avoid churning the past). + +## See Also + +- [oddkit_resolve](klappy://docs/oddkit/specs/oddkit-resolve) — the resolver this audit calls +- [Reference Integrity Audit](klappy://canon/methods/reference-integrity-audit) — the stopgap method this audit retires +- [Ritual Is a Smell](klappy://canon/principles/ritual-is-a-smell) — why this exists at all (correctness shouldn't depend on remembering) +- [Partial Data With Transparency And Background Warm](klappy://canon/principles/partial-data-with-transparency-and-background-warm) — partial-data compliance +- [Deferred Concerns Ledger](klappy://docs/planning/link-rot-deferred-concerns) — terminological drift, projection staleness, epoch gaps, and other deferred work + +## Origin + +Drafted on 2026-04-26 alongside `oddkit_resolve` (DRAFT v4). v1 of this spec proposed four checks (dead-reference + terminological-drift + projection-staleness + epoch-gaps) plus a deprecated-terms registry, epoch-completeness rules, and an `audit_allow:` frontmatter field. v2 (this revision) cut to one check and one allowlist mechanism per the operator's Vodka discipline. The other three checks and supporting registries moved to the deferred-concerns ledger with explicit revisit triggers. From 98d29b9a98d6f6a8db2e0b23d9719ed7df60a456 Mon Sep 17 00:00:00 2001 From: Chris Klapp Date: Sat, 25 Apr 2026 23:09:43 -0400 Subject: [PATCH 3/5] add canon/principles/identity-resolved-by-protocol.md --- .../identity-resolved-by-protocol.md | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 canon/principles/identity-resolved-by-protocol.md diff --git a/canon/principles/identity-resolved-by-protocol.md b/canon/principles/identity-resolved-by-protocol.md new file mode 100644 index 00000000..549a460b --- /dev/null +++ b/canon/principles/identity-resolved-by-protocol.md @@ -0,0 +1,127 @@ +--- +uri: klappy://canon/principles/identity-resolved-by-protocol +title: "Identity Is Resolved By The Protocol — Hardcoded References Are A Cached Lie" +audience: canon +exposure: nav +tier: 1 +voice: principled +stability: graduating +tags: ["principle", "anti-cache-lying", "antifragile", "references", "vodka", "protocol-layer"] +epoch: E0008 +date: 2026-04-26 +derives_from: + - "odd/constraint/anti-cache-lying.md" + - "canon/methods/supersession.md" + - "canon/principles/ritual-is-a-smell.md" +governs: "All cross-document identity references in canon and consumers of canon" +complements: + - "canon/principles/anti-cache-lying.md" + - "docs/oddkit/specs/oddkit-resolve.md" +--- + +# Identity Is Resolved By The Protocol — Hardcoded References Are A Cached Lie + +> An author writing `[link](/page/some-slug)` is hardcoding a location in source. The location will drift; the source will not. Every consumer that reads this source will encounter a broken reference at some point that depends on which consumer renders it, when, and against which version of the canon. The fix is not better discipline. The fix is moving resolution out of source into the protocol that serves it. + +## Summary + +Identity references in canon are written as identity, not as location. Resolution to a current location belongs to the protocol that serves the canon — not to the author writing the reference, not to the consumer rendering it. When resolution lives in the protocol, every consumer gets correct, current, supersession-aware references for free. When resolution lives in source (hardcoded URLs, hardcoded paths, hardcoded index tables), every consumer reproduces drift independently. + +This is anti-cache-lying applied to references. A hardcoded `/page/some-slug` is a cached projection of "this article currently lives at this URL." A hardcoded `[next article](./relative.md)` is a cached projection of "this file is currently at this relative path." A hardcoded `klappy.dev/writings/foo` link in a chat reply is a cached projection of "this URL is currently the public location." All three are lies the moment the underlying state changes — and the underlying state always changes eventually. + +## The Principle + +Three load-bearing claims: + +1. **References in canon source are identities, not locations.** A `klappy://` URI declares "this is what I'm pointing at" — not "this is where it lives." Locations change; identities don't. +2. **The protocol resolves identity to current location at request time.** Whatever serves the canon (oddkit, in current implementation) takes a URI and returns the canonical current answer, walking supersession chains transparently. Consumers never compute locations from identities themselves. +3. **Consumers receive resolved answers and render them.** They do not maintain private indexes. They do not parse path components. They do not interpret URI structure beyond passing it to the resolver. + +When all three hold, references are antifragile to file moves, supersession, renaming, and reorganization. When any one fails, drift returns. + +## Why Hardcoded References Are A Smell + +Every hardcoded reference is a bet that the location won't change. The bet is always wrong eventually. The cost compounds: + +- **For authors**: every move requires a sweep of every consumer to update references. The sweep is never complete. +- **For consumers**: every reader who follows a stale link loses trust. The loss compounds across the corpus. +- **For canon**: supersession metadata exists but cannot help if references don't go through resolution. Authors write `superseded_by` in frontmatter and consumers still hit the dead URL. + +The smell is not "links break." That's the symptom. The smell is **state expressed as authored content** — the same anti-pattern as committing a generated file, the same anti-pattern as caching a derived value in source. Anti-cache-lying names this clearly for derived data; this principle extends it to identity references specifically because identity references are the most common, most invisible, most aggressively-rotting case. + +## What This Excludes + +The principle is about identity references, not all references. It does NOT govern: + +- **External URLs** to systems not under canonical governance (third-party docs, Wikipedia, GitHub repos that aren't ours). Those are genuinely locations, not identities. The HTTP layer handles their resolution. +- **Anchors within a document** (`#section-name`). Internal to a document, no protocol round-trip needed. +- **Code paths** in implementation files (`./utils.ts`). Build systems handle these; they're not canon references. + +A reference is governed by this principle when it points to another canon document. Then identity-not-location applies. + +## How This Manifests + +### In writings + +Every cross-reference is a `klappy://` URI. The renderer (Lovable, claude.ai, agents, future consumers) calls the resolver to get the current URL. Authors never write `/page/...` paths or `./relative.md` paths. + +### In canon docs + +Same as writings. Cross-references between canon, docs, and odd documents use `klappy://` URIs. The `derives_from`, `complements`, `supersedes`, and similar frontmatter fields all use `klappy://` URIs. + +### In renderers + +Consumers walk content for `klappy://` URIs at render time and call the resolver. Static-build consumers without request-time access call a build-time resolver and ship a manifest. Either way, source never contains a resolved URL. + +### In agents + +When an agent surfaces a reference in a response, it does so via `klappy://` URI, not by guessing or hardcoding a klappy.dev URL. The presence layer (chat client, document renderer) resolves at display time. + +## Disconfirmers + +Conditions under which this principle would be retracted: + +1. **A consumer for whom request-time resolution is unacceptable AND no static-build companion is workable.** Every air-gapped agent, every offline export, every one-shot ingest. If this case becomes load-bearing, the principle weakens to "for first-party request-time-capable consumers." +2. **The resolution layer becomes itself a single point of failure that fails more often than it prevents drift.** If oddkit serves wrong resolutions at higher rate than authors would have introduced drift, the principle is net-negative. (Mitigation: the resolver is well-tested before this principle gets enforced; release-validation-gate exists exactly for this.) +3. **A class of reference emerges that is genuinely a location, not an identity.** Currently every cross-canon reference is plausibly an identity. If a real exception arises, the principle's scope narrows. + +The principle survives all three falsifiers as scoping refinements rather than retractions. Real retraction requires the principle producing more drift than it prevents — which would be visible in the dead-reference audit's findings volume over time. + +## Relationship to Other Canon + +- **Anti-cache-lying** (`klappy://odd/constraint/anti-cache-lying`): this principle is its application to identity references. Anti-cache-lying says "don't store derived state as authored content." This principle says "the location of a reference is derived state; therefore don't author it." +- **Supersession** (`klappy://canon/methods/supersession`): five responses to drift. This principle is what makes the metadata actionable — without resolution-by-protocol, `superseded_by` in frontmatter is a fact nobody acts on. +- **Ritual is a smell** (`klappy://canon/principles/ritual-is-a-smell`): "if correctness depends on remembering a procedure, the system has delegated cognition to the wrong party." Hardcoded references depend on authors remembering to update them on every move. That's ritual. The system should act; the operator reviews. + +## Generalizes To + +Same architectural answer applies to other surfaces where state is expressed as authored content: + +- **README index tables** that list current children of a folder. +- **Frontmatter cross-reference fields** (`complements:`, `related:`, `derives_from:`) that hardcode URIs that should resolve at read time. +- **Glossary entries** that reference defining articles. +- **Navigation menus** that hardcode current canonical paths. + +Each of those is a cached projection of state. Each rots independently. The fix is the same — move the projection into the protocol layer. v1 of this principle ships only with link rot fixed via `oddkit_resolve`. The other surfaces become deferred work; when their pain is acute, the principle's prior application gives the architectural answer. + +## What This Demands + +Of authors: write identity, not location. `klappy://` URIs only. + +Of canon governance: ban hardcoded location patterns at lint time (`oddkit_audit` — separate spec) so the principle is mechanically enforced. + +Of the protocol (oddkit): provide one canonical resolution surface (`oddkit_resolve` — separate spec). Be partial-data-compliant. Be supersession-aware. Be backward-compatible. + +Of consumers: call the resolver. Don't parse URIs. Don't maintain private indexes. Render whatever the resolver returns. + +## See Also + +- [Anti-Cache Lying](klappy://odd/constraint/anti-cache-lying) — the parent constraint this principle extends +- [Supersession](klappy://canon/methods/supersession) — the metadata this principle activates +- [Ritual Is a Smell](klappy://canon/principles/ritual-is-a-smell) — why discipline alone isn't enough +- [oddkit_resolve](klappy://docs/oddkit/specs/oddkit-resolve) — the protocol mechanism that implements this principle +- [oddkit_audit](klappy://docs/oddkit/specs/oddkit-audit) — the enforcement mechanism that prevents regression + +## Origin + +Graduated on 2026-04-26 from recurring broken-link reports on klappy.dev that traced to hardcoded `/page/...` and relative-path patterns in source markdown. The April 9, 2026 reference integrity audit found 85 broken references; a 2026-04-24 scan of `writings/*.md` found 11 more from articles authored just weeks earlier. Discipline alone had failed multiple times across multiple sessions. This principle names the architectural reason — identity is not location, and location is derived state — so future surfaces inherit the same answer rather than re-discovering it through their own incidents. From 6e9656e4aa9c2825f4d0d30b6de46abb0df0c9fa Mon Sep 17 00:00:00 2001 From: Chris Klapp Date: Sat, 25 Apr 2026 23:09:44 -0400 Subject: [PATCH 4/5] add docs/planning/link-rot-deferred-concerns.md --- docs/planning/link-rot-deferred-concerns.md | 206 ++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/planning/link-rot-deferred-concerns.md diff --git a/docs/planning/link-rot-deferred-concerns.md b/docs/planning/link-rot-deferred-concerns.md new file mode 100644 index 00000000..166a1193 --- /dev/null +++ b/docs/planning/link-rot-deferred-concerns.md @@ -0,0 +1,206 @@ +--- +uri: klappy://docs/planning/link-rot-deferred-concerns +title: "Deferred Concerns — Link-Rot Campaign Cuts and Revisit Conditions" +audience: docs +exposure: nav +tier: 2 +voice: neutral +stability: stable +tags: ["planning", "link-rot", "deferred", "use-only-what-hurts", "vodka", "epoch-8"] +epoch: E0008 +date: 2026-04-26 +derives_from: + - "docs/oddkit/specs/oddkit-resolve.md" + - "docs/oddkit/specs/oddkit-audit.md" + - "canon/principles/identity-resolved-by-protocol.md" +governs: "What was cut from the link-rot campaign and when each cut item should graduate back to active work" +--- + +# Deferred Concerns — Link-Rot Campaign Cuts and Revisit Conditions + +> The link-rot campaign was redrafted to a Vodka-disciplined four-artifact shape. This doc captures what was cut, why each was real, and the explicit pain trigger that would justify graduating it back into active work. Per Use Only What Hurts: don't build infrastructure ahead of the pain that justifies it. Per memory-against-amnesia: don't lose the thinking either. + +## Why This Ledger Exists + +When campaign scope is cut, two failure modes are equally bad: + +1. **Forgetting the cut work was ever considered.** Future incidents re-discover the same problems and re-design solutions from scratch. +2. **Treating cut items as latent work that distorts roadmap planning.** Items linger in queues, accrete priority, and pull attention away from observed pain. + +This ledger threads the needle — every cut item has an explicit revisit condition. If the condition triggers, the item graduates. If the condition doesn't trigger for a year, the item probably wasn't real pain after all and can be archived. + +## Cut Items + +Each item below was drafted (or partly drafted) during the v1–v3 spec iterations. v4 cut them. They live here. + +--- + +### 1. `oddkit_resolve_batch` action + +**What it was**: a build-time companion action taking an array of URIs and returning resolved results in input order. Purpose: serve static-build consumers (Lovable today) that can't make request-time MCP calls. + +**Why it was real**: Lovable is a real consumer that ships a static build. Without batch resolution, either (a) Lovable can't use the resolver at all, or (b) Lovable's build pipeline makes N independent MCP calls, which is slow and rate-limit-fragile. + +**Why it got cut**: no consumer has demonstrated the pain yet. Lovable currently has zero `klappy://` URIs to resolve. Building a batch action before any consumer needs it is infrastructure-ahead-of-pain. + +**Revisit condition**: a real consumer — Lovable, or another static-build renderer — has at least 20 `klappy://` URIs in its build artifacts AND demonstrates measurable pain (build slowness, rate-limit hits, or operator frustration with N round-trips). + +**When triggered, the work**: re-draft `oddkit_resolve_batch` per the v3 spec section. The design is preserved in this ledger; implementation is the only fresh work. + +--- + +### 2. `resolve_links` flag on `oddkit_get` and `oddkit_search` + +**What it was**: an optional flag that returns resolved-link metadata alongside content for every `klappy://` URI in the document. Purpose: save consumers from making N follow-up `oddkit_resolve` calls per fetched document. + +**Why it was real**: a renderer fetching a writings doc with 12 internal references has to make 12 resolver calls to render the page. That's 13 round-trips when 1 could suffice. + +**Why it got cut**: same pattern as the batch action — building convenience before observed pain. Current consumers don't render writings via the MCP; they render via static build. The `resolve_links` flag's beneficiary doesn't exist yet. + +**Revisit condition**: a request-time renderer (a future consumer, not Lovable today) is in production and its render latency is measurably impacted by per-URI resolver round-trips. + +**When triggered, the work**: add the flag to `oddkit_get` and `oddkit_search` envelopes per v3 spec section. Backward-compatible (omitted flag returns unchanged response). + +--- + +### 3. `aliases` frontmatter field + +**What it was**: an optional array on a canon doc declaring alternate URIs that resolve to it. Purpose: support migrations where a URI changes meaningfully without breaking existing references. + +**Why it was real**: when an article's slug is renamed, all existing references to the old slug break. Aliases let the rename happen without breaking the world. + +**Why it got cut**: supersession already covers this case. When you rename `klappy://writings/old-slug` to `klappy://writings/new-slug`, you mark the old as `superseded_by: klappy://writings/new-slug`. The resolver walks the chain transparently. `aliases` is a convenience for a different mental model (one current name, many historical names) but doesn't solve a problem supersession doesn't already solve. + +**Revisit condition**: a real case emerges where supersession-as-rename feels semantically wrong (e.g., a single article should be findable under multiple equally-current names without one being "the" canonical). Until then, the supersession path is sufficient. + +**When triggered, the work**: add `aliases:` to frontmatter schema and handle in resolver lookup. v3 spec section preserves the design. + +--- + +### 4. `supersession_response` frontmatter field + +**What it was**: explicit declaration of which of the five supersession responses (`tolerate` / `observe` / `graduate` / `replace` / `regenerate`) applies to a particular `superseded_by` link. Purpose: let the resolver return the right answer for non-`replace` cases. + +**Why it was real**: per `klappy://canon/methods/supersession`, drift has five valid responses, not just "the new one replaces the old." `graduate` (original retained as canonical for its scope) and `tolerate` / `observe` (both apply) are real cases where transparent redirect is wrong. + +**Why it got cut**: every existing `superseded_by` in canon today is implicitly `replace`. There's no real case in the index where transparent redirect produces wrong consumer behavior. The field would ship as "always set to `replace` because that's the only case anyone uses." Bloat. + +**Revisit condition**: a real article wants to declare `superseded_by` with non-replace semantics AND a consumer's behavior under transparent redirect is observably wrong. + +**When triggered, the work**: add the field; update resolver to consult it. v3 spec section preserves the design (per-response rendering table, default to `replace`). + +--- + +### 5. Identity-by-meaning queries (`klappy://meaning/` URIs and structured `meaning:` input) + +**What it was**: the resolver accepts a structured query expressing intent (title hint, topic, tags, audience filter) instead of a URI. Returns the best-matching canonical article. Purpose: handle author URI typos, supersession-blind references, and "I want to point at the article about X without knowing its current URI." + +**Why it was real**: the operator explicitly named this as the ideal primitive ("identity by meaning not URI is ideal as it handles supersession naturally"). The mental model — "I'm pointing at the idea, not the location of the idea" — is genuinely cleaner than URI-based references. + +**Why it got cut**: no consumer needs it for v1. Authors writing `klappy://writings/` have a stable convention that already works as long as they don't typo. Meaning queries solve a problem (typo tolerance, automatic supersession traversal even without `superseded_by` set) that doesn't exist in observed practice yet. + +**Revisit condition**: the dead-reference audit shows a sustained pattern of typo-rate that URI-only resolution can't handle gracefully — i.e., authors are writing wrong URIs frequently enough that meaning fallback would prevent meaningful churn. Or: a consumer use case emerges where the author legitimately doesn't know the URI and wants to point at "the article about cognitive saturation." + +**When triggered, the work**: add meaning resolution to the resolver per v3 spec section. The principle (identity-by-meaning is ideal) is already canonized in `klappy://canon/principles/identity-resolved-by-protocol`; the implementation is the fresh work. + +--- + +### 6. `oddkit_audit` terminological-drift check + +**What it was**: detect deprecated vocabulary (e.g., "Lane Self-Containment", "OLDC+H") in non-archival content. Read against a `canon/terminology.md` registry of deprecated terms with replacements. + +**Why it was real**: terminology drifts as canon evolves. The projection inventory (March 21, 2026) explicitly cited `canon/constraints/README.md`'s outline still saying "Lane Self-Containment" weeks after the rename. Human readers and agents both encounter stale terms and lose ground-truth confidence. + +**Why it got cut**: solves a different problem from link rot. The reported pain was broken links, not stale vocabulary. Bundling them into one audit conflated concerns. Terminological drift deserves its own thin solution when its pain is acute. + +**Revisit condition**: a deprecated-term incident causes confusion or lost time. Or: the terminology registry would have ≥10 entries (signal that drift volume is real). Or: an author or agent surfaces frustration about stale vocabulary. + +**When triggered, the work**: thin separate action `oddkit_terminology_audit` (NOT bundled into `oddkit_audit`) with the registry pattern from v1 of the audit spec. Its own CI check, its own severity defaults. + +--- + +### 7. `oddkit_audit` projection-staleness check + +**What it was**: detect committed projection files that differ from regenerated form. Read against `klappy://docs/audits/projection-inventory-2026-03-21` to know which files are projections. + +**Why it was real**: per anti-cache-lying, committed projections are cached lies. The projection inventory enumerates ~15 pure projections + 12 hybrids that are actively rotting. Multiple agents and humans have hit stale README index tables. + +**Why it got cut**: requires regeneration tooling that doesn't exist for most projections. Without regenerators, the check could only flag presence (per inventory) — limited value. Real fix is generating projections JIT, not detecting their staleness in source. The bigger architectural answer (eliminate committed projections entirely) is its own campaign. + +**Revisit condition**: regeneration tooling exists for at least one projection file (e.g., a script that builds `writings/README.md` from frontmatter). Or: a projection-staleness incident causes meaningful pain (agent navigation breaks, reader follows a stale index, audit comes back ugly). + +**When triggered, the work**: separate thin action `oddkit_projection_audit` that consumes the inventory, calls per-projection regenerators, and diffs. Or — preferable per anti-cache-lying — eliminate the projections entirely and serve them JIT. The latter is its own campaign, not a thin action. + +--- + +### 8. `oddkit_audit` epoch-gap check + +**What it was**: detect missing OLDC+H / DOLCHE+O artifacts within an epoch's expected ledger. Read against an epoch-completeness rules document. + +**Why it was real**: the epistemic ledger pattern expects certain artifact types per epoch. An epoch with handoffs but zero decisions, or a handoff referencing a constraint that doesn't exist, indicates documentation gaps. + +**Why it got cut**: zero observed instances of epoch-gap pain in current operation. Inventing rules ("an epoch SHOULD have at least one decision") and then enforcing them is ritual-as-compensating-control before any ritual has been needed. + +**Revisit condition**: an epoch transition reveals a real documentation gap that costs time or trust. Or: a session ends with a handoff that points at non-existent canon and the gap goes undetected for ≥1 day. + +**When triggered, the work**: separate thin action `oddkit_epoch_audit` with completeness rules tuned to the actual gap that triggered the work. Don't pre-design the rule set; let real incidents inform it. + +--- + +### 9. `audit_allow:` frontmatter field + +**What it was**: file-level audit suppression (vs the line-level `` directive that ships in v1). + +**Why it was real**: a template file with 30 placeholder URIs would need 30 line-level directives. File-level suppression handles that case in one declaration. + +**Why it got cut**: no template file with 30 placeholder URIs exists yet. Line-level directives handle every observed case. + +**Revisit condition**: a real file emerges where line-level directives produce >10 entries AND the suppression is legitimate. Examples: a curated examples doc, an intentionally-incomplete planning doc. + +**When triggered, the work**: add `audit_allow:` to frontmatter schema; update audit to honor it. Small, additive. + +--- + +### 10. CI pre-commit hook + +**What it was**: local pre-commit hook calling a fast subset of audit checks against staged `.md` files for sub-second author feedback. + +**Why it was real**: CI round-trip latency is real. Authors who get feedback in seconds adjust faster than authors who get feedback after pushing. + +**Why it got cut**: complicates the pattern. Calling the live worker on every commit is too slow; building a static local subset duplicates audit logic; mismatches between local and CI become a class of confusion. CI alone is sufficient as v1 enforcement. + +**Revisit condition**: PR cycle time is measurably hurting author velocity AND audit findings are frequent enough that pushing-then-fixing is producing churn. + +**When triggered, the work**: design a thin local check (probably static, no live worker call) that runs the dead-reference and legacy-link-pattern rules against staged files only. Accept that local-vs-CI mismatches will exist and document the precedence. + +--- + +## What's NOT Deferred + +For clarity — these were NOT cut and are still in the active campaign: + +- `oddkit_resolve` action (v4 KISS shape) +- `oddkit_audit` action with single dead-reference check (v2 KISS shape) +- `canon-quality.yml` workflow with soft-block then hard-block escalation +- `klappy://canon/principles/identity-resolved-by-protocol` tier-1 doc +- Writings cleanup (convert legacy patterns to `klappy://` URIs) +- Re-audit the 49 from the April 9 audit using the new resolver + +Six items in the active campaign. Ten items deferred. The cut roughly doubled the deferred-to-active ratio in the campaign's favor, which matches the Vodka discipline of "remove until it would break." + +## Graduation Process + +When a deferred item's revisit condition triggers: + +1. Confirm the trigger is real (not a one-off; not premature). Use Only What Hurts means *acute* pain, not *theoretical* pain. +2. Open a single-item campaign for the deferred work. Don't bundle multiple deferred items together unless they share a real dependency. +3. Apply the same Vodka discipline. Just because we wrote a v3 design for it doesn't mean v3 is the right shape when it ships. +4. Update this ledger: mark the item as graduated, note the trigger, link to the campaign or PR. + +## Archive Process + +If a deferred item's revisit condition hasn't triggered after one calendar year, the item probably wasn't real pain. Archive it from this ledger to a lower-priority lookup; remove from active mental load. Future incidents can resurface the design from history if needed. + +## Origin + +Drafted on 2026-04-26 alongside the v4 KISS revision of the link-rot campaign. The operator surfaced that the v3 campaign violated Vodka principles by building infrastructure ahead of observed pain. Cutting to four artifacts left ten real concerns behind; this ledger captures them so they're not lost. The ledger itself is not load-bearing — it's a memory aid. The active artifacts (resolver, audit, workflow, principle) carry the load. From cbd06d6f0b5b0c2d744143e7854bfb0b7253f90f Mon Sep 17 00:00:00 2001 From: Chris Klapp Date: Sat, 25 Apr 2026 23:09:45 -0400 Subject: [PATCH 5/5] add docs/planning/link-rot-elimination-campaign.md --- .../planning/link-rot-elimination-campaign.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/planning/link-rot-elimination-campaign.md diff --git a/docs/planning/link-rot-elimination-campaign.md b/docs/planning/link-rot-elimination-campaign.md new file mode 100644 index 00000000..c87499d7 --- /dev/null +++ b/docs/planning/link-rot-elimination-campaign.md @@ -0,0 +1,117 @@ +--- +uri: klappy://docs/planning/link-rot-elimination-campaign +title: "Campaign Sequencing — Link-Rot Elimination (KISS)" +audience: docs +exposure: nav +tier: 2 +voice: neutral +stability: stable +tags: ["planning", "campaign", "link-rot", "vodka", "kiss", "epoch-8"] +epoch: E0008 +date: 2026-04-26 +derives_from: + - "docs/oddkit/specs/oddkit-resolve.md" + - "docs/oddkit/specs/oddkit-audit.md" + - "canon/principles/identity-resolved-by-protocol.md" + - "docs/planning/link-rot-deferred-concerns.md" +governs: "Sequencing of the four artifacts that together eliminate the link-rot bug class" +supersedes: "DRAFT v1 (2026-04-26, six-phase pre-Vodka-cut version)" +--- + +# Campaign Sequencing — Link-Rot Elimination (KISS) + +> Three phases. Four artifacts. The minimum that closes the bug class. Everything else is deferred per `klappy://docs/planning/link-rot-deferred-concerns`. + +## The Goal + +Eliminate the broken-link bug class on `klappy.dev` such that: + +1. Every consumer of canon resolves `klappy://` URIs through the protocol — no consumer hardcodes URLs. +2. CI mechanically prevents new dead references and legacy markdown link patterns from merging. +3. A canon principle names the architectural answer so future surfaces inherit it. + +## Phase Overview + +``` +Phase 1 — Foundation (3 PRs, parallelizable) + ├── PR-1.1 — Resolver spec (klappy://docs/oddkit/specs/oddkit-resolve) + ├── PR-1.2 — Audit spec (klappy://docs/oddkit/specs/oddkit-audit) + └── PR-1.3 — Tier-1 principle + deferred ledger + (klappy://canon/principles/identity-resolved-by-protocol + + klappy://docs/planning/link-rot-deferred-concerns + + this campaign doc) + +Phase 2 — Implementation (3 PRs, serial) + ├── PR-2.1 — Implement oddkit_resolve action + │ ⚠️ Release-validation-gate per E0008.3 + ├── PR-2.2 — Convert legacy patterns in writings/ to klappy:// URIs + │ Re-audit the 49 from April-9 audit; fix or delete + └── PR-2.3 — Implement oddkit_audit action + ⚠️ Release-validation-gate per E0008.3 + +Phase 3 — Enforcement (2 PRs, serial after observation) + ├── PR-3.1 — canon-quality.yml workflow (soft-block this cycle) + └── PR-3.2 — Flip vars.AUDIT_ENFORCEMENT_MODE to "hard" + (config flip after observation cycle) +``` + +Eight PRs total. Critical path is six PRs (PR-1.1 → PR-1.2 → PR-2.1 → PR-2.3 → PR-3.1 → PR-3.2). The other two (PR-1.3, PR-2.2) ship in parallel. + +## Dependencies + +- **PR-1.1, PR-1.2, PR-1.3**: no prerequisites. Specs and canon docs. Can ship in parallel. +- **PR-2.1**: requires PR-1.1 (the spec is the contract). +- **PR-2.2**: requires PR-2.1 (the new URIs need to actually resolve when reviewers click them). +- **PR-2.3**: requires PR-2.1 (audit calls the resolver). +- **PR-3.1**: requires PR-2.3 (workflow calls the audit). +- **PR-3.2**: requires one observation cycle of PR-3.1 in soft-block mode. + +## Release-Validation-Gate Application + +| Phase | RV-Gate Required? | Why | +|-------|-------------------|-----| +| Phase 1 | No | Specs and canon docs; no load-bearing surface. | +| Phase 2 | **Yes for PR-2.1 and PR-2.3** | New action surfaces. Validators verify each action's contract end-to-end against live prod. | +| Phase 3 | No (PR-3.1) / No (PR-3.2) | Workflow YAML is reviewable as code. Hard-block flip is a config var change. | + +Plan Phase 2 PR cadence with validator dispatch time included. + +## Operator Decision Points + +Three points where the campaign waits on input: + +1. **Now** — greenlight to start Phase 1. +2. **After PR-3.1 lands** — observation cycle length. Recommendation: 3–5 PRs through the gate to see real signal. +3. **End of observation cycle** — greenlight to flip the enforcement mode variable (PR-3.2). + +Everything else is sequenced. These three are operator-judged. + +## Definition of Done + +The campaign is complete when: + +1. ✅ `oddkit_resolve` is in production. URIs resolve correctly. +2. ✅ Writings have zero legacy markdown link patterns. +3. ✅ `oddkit_audit` is in production. Dead-reference and legacy-link-pattern rules operational. +4. ✅ `canon-quality.yml` is in `hard` enforcement mode. Required status check on `main`. +5. ✅ Tier-1 canon principle is published. + +Five conditions. Stopping anywhere short leaves the durability gap open. + +## What This Campaign Does NOT Cover + +Per `klappy://docs/planning/link-rot-deferred-concerns`: + +- `oddkit_resolve_batch` action +- `resolve_links` flag on get/search +- `aliases` and `supersession_response` frontmatter fields +- Identity-by-meaning queries +- Terminological-drift, projection-staleness, epoch-gap audit checks +- `audit_allow:` frontmatter field +- Pre-commit hook + +Each of these is a real concern with a real revisit condition. None is in this campaign. When their pain is acute, each becomes its own thin separate campaign. + +## Origin + +Drafted on 2026-04-26 as the v2 KISS revision of the campaign. The v1 version had six phases and ~15 PRs and violated Vodka principles by pre-building infrastructure for hypothetical pain. v2 cuts to three phases, eight PRs, and the four-artifact minimum. The cut work moved to the deferred-concerns ledger with explicit revisit triggers. Operator's framing: "Vodka principles, KISS, antifragile, maintainable, consistent."