Graft keeps a derived, disposable code graph in sync with a content hash rather than a calendar, and keeps a protected block on every regenerated node. This KB is the opposite kind of store — durable, curated, built from material that cannot be regenerated — but several of Graft's mechanisms port cleanly, and two of them close real gaps here. Schema 1.5 is additive: every 1.4 page remains valid. 1. `## Crux` — verbatim source excerpts alongside the synthesis. A summary can drift silently; a quote either still matches its source or it does not. Lets `ckb-retrieve` ground an answer without a round-trip to the archive, and makes drift mechanically detectable. 2. `## Notes` — human-authored and protected everywhere. Closes a real gap: `ckb-index-external` regenerates connector pages wholesale, so an annotation written there was previously destroyed on the next refresh. 3. `source_fingerprint`/`source_checked` — a digest of the material a page was built from. Freshness by date says a page has aged; a fingerprint says whether its evidence moved. Most valuable for connector-backed libs, where documents change with no notice. 4. `lint_report.py --quick` — a deterministic one-line session-start signal, wired into Rule E next to the existing `git status` check. 5. In-degree as a rank-fusion signal in `ckb-retrieve`, weighted below 1.0: centrality is a prior, not evidence. 6. Blast radius — a new `ckb-ingest` step walking the graph backwards from touched entities to find what the incoming material contradicts, before writing anything. Ingest was additive-first, which is how a wiki accumulates two pages that quietly disagree. 7. Edge vocabulary in `wiki/graph/index.md` rewritten as a question per verb, and completed: `part_of` was written by `ckb-code-map` but never declared. Added `produces`, `configures`, `validates`, `implements`. Lint gains checks 12 (fingerprint drift), 13 (crux verbatimness) and 14 (the protected-Notes rule), verified against a synthetic fixture covering stale digests, missing sources, fabricated quotes and paraphrased evidence. Not adopted: the gitignored regenerable store, the MCP server and CLI daemon, tree-sitter parsing, statusline hooks, telemetry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
354 lines
18 KiB
Markdown
354 lines
18 KiB
Markdown
---
|
|
name: ckb-lint
|
|
description: Health-check the wiki/ — conformance, freshness, confidence decay, retention sweep, supersession detection, orphan detection, graph consistency, index/log consistency, and error-book entries — auto-fixing what it safely can and reporting the rest, then reminding the user to review and sync to origin. Also runs the same checks against each connector-backed libs/<name>/'s own generated index (see ckb-index-external), plus a source.yaml validity check, without ever touching a git-copy lib or a connector's source.yaml itself. Use when the user says "Lint", "health-check the wiki", "check the wiki", or asks for a periodic/scheduled wiki health check. This is the maintenance workflow routed by CLAUDE.md/AGENTS.md, distinct from `ckb-ingest` (which adds new content) and `ckb-sync-changes` (a pure git-level operation with no wiki synthesis at all).
|
|
---
|
|
|
|
# Lint skill
|
|
|
|
## Purpose
|
|
|
|
Health-check the local, mutable `wiki/` for structural rot: missing or
|
|
malformed frontmatter, stale pages, decayed confidence, orphaned or
|
|
archivable content, broken graph edges, and index/log drift. This is
|
|
maintenance, not growth — it never adds new knowledge the way `ckb-ingest`
|
|
does; it only checks and repairs the shape of what's already there.
|
|
|
|
Implemented as a skill (rather than living inline in
|
|
`CLAUDE.md`/`AGENTS.md`) so the full checklist only loads into context when
|
|
actually invoked — on demand, or on a schedule if the user has set one up.
|
|
|
|
The **detection** half of this checklist is mechanical — a fixed set of
|
|
frontmatter, date, link, graph, and index/log rules over a tree that keeps
|
|
growing — so it runs as a read-only Python script rather than as per-page
|
|
model reasoning (Step 0 below). What the script cannot decide is what this
|
|
skill is actually for: which findings to auto-fix, which to leave for the
|
|
user, whether two pages genuinely supersede one another, and whether a
|
|
cluster of findings is a systemic pattern worth an error-book entry. Read
|
|
the script's report, then do that work.
|
|
|
|
This skill only ever writes within `wiki/` (including moves into
|
|
`wiki/archived/`) and, for any connector-backed `libs/<name>/` (one with a
|
|
`source.yaml` — see `CLAUDE.md`/`AGENTS.md` directory contract) where this user has
|
|
local `access: write` (see `ckb-index-external`), within that connector's
|
|
own agent-owned generated index (`index.md`/`entities/`/`graph/`/`log.md`).
|
|
For a connector-backed `libs/<name>/` where this user is read-only (the
|
|
default), findings against that index are reported only, never auto-fixed
|
|
— the same access gate `ckb-index-external` applies to rebuilding applies
|
|
here too. It never touches `linked/`, a git-copy `libs/<name>/`, or a
|
|
connector's `source.yaml`/`source.local.yaml` — those stay immutable or
|
|
user-owned — and it never touches this repo's own git remote; pushing the
|
|
resulting changes is a separate, explicit step (see the final step below).
|
|
|
|
## Trigger phrases
|
|
|
|
Use this skill when the user says:
|
|
- "Lint"
|
|
- "health-check the wiki" / "check the wiki"
|
|
- "run the periodic/scheduled wiki check"
|
|
|
|
Do **not** use this skill for "Ingest" / "Sync the wiki" (that's
|
|
`ckb-ingest` — turning raw material into new wiki pages) or for "sync
|
|
changes" / "sync with git" (that's `ckb-sync-changes` — reconciling this
|
|
repo's own commit history with `origin`, no wiki content involved).
|
|
|
|
## How to run this skill
|
|
|
|
Work through each check in order. A malformed page breaks every later
|
|
check's ability to reason about it, so conformance comes first.
|
|
|
|
Checks 1-9 below are written against `wiki/`, but the same checks also run
|
|
a second time against each connector-backed `libs/<name>/`'s own generated
|
|
index — scoped entirely to that one connector's `index.md`/`entities/`/
|
|
`graph/`/`log.md`. Auto-fixes there only happen where this user has local
|
|
`access: write` for that source (see `ckb-index-external`); everywhere
|
|
read-only, findings are reported but not applied. Findings and fixes there
|
|
are logged in that connector's own `log.md`, never in `wiki/log.md`. A
|
|
git-copy `libs/<name>/` (no `source.yaml`) and any `source.yaml`/
|
|
`source.local.yaml` are never touched by any of these checks.
|
|
|
|
### 0 — Run the checker script
|
|
|
|
From the repository root:
|
|
|
|
```bash
|
|
python3 "<skill-dir>/scripts/lint_report.py"
|
|
```
|
|
|
|
Resolve `<skill-dir>` to this skill's own directory. Flags: `--scope
|
|
wiki|libs|all` (default `all`) to limit which trees are checked; `--today
|
|
YYYY-MM-DD` to pin the date for reproducible runs; `--json` for a
|
|
machine-readable report; `--quick` for the one-line session-start summary
|
|
Rule E calls (conformance, freshness, and source drift only — it prints a
|
|
single line and does none of the work below). Exit code is `0` when nothing was found and `1`
|
|
when there are findings — read the report either way.
|
|
|
|
The script is strictly read-only: it writes nothing, moves nothing, and
|
|
runs no `git` commands. It covers the mechanical detection in checks 1, 2,
|
|
3, 4, 6, 7, 8, 10, 12, and 13 below — plus the structural half of check 5
|
|
(decision records) — across `wiki/` and every connector-backed
|
|
`libs/<name>/` (reporting each one's `access:` level so you know where a
|
|
fix is even permitted). Check 9, the semantic half of check 5, and every
|
|
auto-fix-vs-report decision, are yours.
|
|
|
|
The numbered checks below stay as the reference for *what each finding
|
|
means and what to do about it* — you don't need to re-derive the detection
|
|
by hand. Do read the pages the script flags: a finding is a pointer to a
|
|
page that needs a decision, not the decision itself.
|
|
|
|
### 1 — Conformance check
|
|
|
|
The script flags every non-reserved `.md` file under `wiki/` (i.e.
|
|
excluding `index.md` and `log.md`) whose frontmatter is unparseable,
|
|
absent, or missing a non-empty `type`. Treat flagged pages as unreliable
|
|
input for the checks below rather than guessing at their intended
|
|
type/content, and fix the frontmatter before acting on any later finding
|
|
about the same page.
|
|
|
|
### 2 — Freshness check
|
|
|
|
The script reports every page whose `last_updated` exceeds its
|
|
`freshness_window_days`, and by how much. Suggest the user confirm or
|
|
update the content — don't silently rewrite stale content yourself.
|
|
|
|
### 3 — Confidence decay
|
|
|
|
The script reports pages already below 0.3 `confidence` (and any
|
|
unparseable value). Decaying `confidence` on pages not reinforced by a new
|
|
source since the last check is a write and a judgment call, so it stays
|
|
yours — the script only surfaces where the floor has been crossed.
|
|
|
|
### 4 — Retention sweep
|
|
|
|
The script reports `retention: low` pages older than 2x their
|
|
`freshness_window_days` as archive candidates. Move them into
|
|
`wiki/archived/` — never delete, always move — and log each move (see
|
|
Rule B in `CLAUDE.md`/`AGENTS.md`) with a note explaining why. Check each
|
|
candidate against recent use before moving it (see Edge cases).
|
|
|
|
### 5 — Supersession and decision records
|
|
|
|
The script checks the *structure* of every `type: decision` page: a status
|
|
from the vocabulary, a `decided_on` date where the status implies one, a
|
|
`decided_by` (`unknown` counts — an omitted field doesn't), `affects` targets
|
|
and supersession links that resolve, supersession set on **both** sides,
|
|
`status: superseded`/`reversed` matched by a `superseded_by`, unique
|
|
four-digit decision numbers, and any `review_on` date that has passed. Fix
|
|
those directly — they're mechanical. A `review_on` that has come due is not a
|
|
defect, though: report it so the user can decide whether to revisit, and never
|
|
change a decision's substance yourself (`ckb-decide` → decisions are
|
|
append-only).
|
|
|
|
The *semantic* half stays yours: when two pages appear to cover the same
|
|
entity or the same choice, check for contradictions. If one is clearly
|
|
newer/better corroborated, add `supersedes` / `superseded_by` links between
|
|
them. Preserve the older page rather than deleting it, but mark it stale. For
|
|
two decisions, that judgment is exactly what the script can't make — it can
|
|
tell you a link is one-sided, not that a link should exist.
|
|
|
|
### 6 — Orphan detection
|
|
|
|
The script reports pages with no inbound links (`[[wikilinks]]` or
|
|
`[markdown](path.md)` references from anywhere else in the tree). For each
|
|
one, either add backlinks from relevant pages where an obvious connection
|
|
exists, or move the orphan to `wiki/archived/` with a log note if no
|
|
natural backlink does — that choice is the judgment the script leaves you.
|
|
|
|
### 7 — Graph consistency
|
|
|
|
The script reports every edge in `wiki/graph/edges.json` whose `from`/`to`
|
|
does not resolve to an existing page (plus malformed edges and invalid
|
|
JSON). Remove or fix them; note what was removed rather than silently
|
|
dropping entries.
|
|
|
|
### 8 — Index/log consistency
|
|
|
|
The script reports subdirectories holding pages but no `index.md`, index
|
|
files that don't list a page sitting next to them, and any change recorded
|
|
in both a subdirectory `log.md` and the root `wiki/log.md` (per the
|
|
Recursive Index & Log Convention). Fix missing index entries and duplicate
|
|
log entries directly — these are the safest auto-fixes on the list.
|
|
|
|
### 9 — Error Book entry
|
|
|
|
Record any systemic issue found above (a repeated broken pattern, a
|
|
recurring format mismatch, the same kind of orphan appearing again) in
|
|
`wiki/error-book.md` with its root cause, the fix applied, and the derived
|
|
constraint that should prevent recurrence. A one-off, isolated issue
|
|
doesn't need an Error Book entry — this is for patterns, not incidents.
|
|
|
|
### 10 — External source config check
|
|
|
|
The script validates each `libs/<name>/source.yaml` (non-empty `connector`
|
|
and `location`; non-empty `store` and `location` inside any `index:` block;
|
|
a positive integer `refresh_interval_days`) and flags a `libs/<name>/` that
|
|
ambiguously holds both real content files and a `source.yaml`. All of this
|
|
is **report only** — `source.yaml` is never edited by any skill, and the
|
|
ambiguous-content case is a configuration conflict for the user to resolve,
|
|
not something to guess at.
|
|
|
|
It also reports any connector-backed source whose generated index is
|
|
overdue — newest `last_updated` in `libs/<name>/` older than its
|
|
`refresh_interval_days` (default 30) — with how overdue it is, since a
|
|
source two days past a 7-day interval is a different situation from one six
|
|
months past a 30-day one. Relay that the same way whether or not this user
|
|
has write access: a read-only user can't fix it, but knowing which source to
|
|
chase the admin about is the actionable part. Never re-index here; that's
|
|
`ckb-index-external`'s job, and suggesting it is as far as this check goes.
|
|
|
|
Don't flag a missing `libs/<name>/source.local.yaml` as an issue — its
|
|
absence is the correct, read-only default (see `ckb-index-external`), not
|
|
a gap to report or fix. The script follows the same rule: it reads that
|
|
file only to label each connector `access: read-only` / `access: write` in
|
|
its report.
|
|
|
|
### 11 — Installed module checks
|
|
|
|
If `ckb.yaml` lists installed modules, run whatever extra checks those
|
|
modules define, and skip this step entirely when the file is absent or
|
|
empty. Modules are optional by design: their absence is never a finding.
|
|
|
|
For the `software` module specifically:
|
|
|
|
- **Stale code maps** — a `type: repository` page whose `commit` is behind
|
|
the current `HEAD` of its `src/` clone. Report how far behind; suggest
|
|
`ckb-code-map`. A missing clone is **not** a finding — `src/` is
|
|
gitignored and expected to be empty on a fresh checkout.
|
|
- **Spec drift** — the four checks `ckb-spec` defines: root specs with
|
|
empty `implemented_by`, repo-level specs implementing a missing or
|
|
archived parent, one-sided `implements`/`implemented_by` links, and root
|
|
specs older than the mapped commit of every repo implementing them.
|
|
- **Remote-less repos** — a mapped repository page with no `resource:`,
|
|
meaning a gitignored clone that exists nowhere else.
|
|
|
|
All report-only. Never auto-fix any of these: each is a statement about
|
|
intent, and only the user knows which side is correct.
|
|
|
|
### 12 — Source fingerprint drift
|
|
|
|
The script recomputes the `sha256` of every local file cited in a page's
|
|
`## Sources` and compares it against the digest recorded there. Three
|
|
findings come out of it, and they are not the same problem:
|
|
|
|
- **`CHANGED since this page was built`** — the file the page was
|
|
synthesized from has been edited. This outranks every freshness finding
|
|
on the list: check 2 says a page has *aged*, this says its evidence has
|
|
*moved*. Read both, and either re-verify the page's claims against the
|
|
new content and re-stamp the digest, or flag it for `ckb-ingest`. Never
|
|
re-stamp a digest without reading what changed — that converts a real
|
|
finding into a silent lie.
|
|
- **`does not exist`** — a cited source was moved, renamed, or deleted. The
|
|
page now rests on nothing. Find where the source went if you can; if you
|
|
can't, say so on the page rather than leaving a citation that looks
|
|
valid.
|
|
- **`has no fingerprint recorded`** — a `## Sources` bullet predating this
|
|
convention, or written by hand. Safe to auto-fix *only* when the page has
|
|
not otherwise changed: compute the digest and record it with today's
|
|
`source_checked`. If the page is also flagged by check 2 or 13, fix those
|
|
first — stamping a digest onto a page you haven't verified just freezes
|
|
the drift in place.
|
|
|
|
An `etag:`/`mtime:` fingerprint (connector-sourced, per
|
|
`ckb-index-external`) is skipped here: there is nothing local to recompute.
|
|
Those are verified by re-indexing that source, not by this script.
|
|
|
|
### 13 — Crux verbatimness
|
|
|
|
`## Crux` is quoted evidence, so it admits a check no synthesized prose
|
|
does: the quote either still appears in the source or it doesn't. The
|
|
script flags four cases:
|
|
|
|
- **Quote not found verbatim in the cited source** — the strongest finding
|
|
the linter produces. The page asserts, in quotation marks, something its
|
|
source does not say. Either the source was edited (check 12 usually fires
|
|
alongside; fix them together) or the quote was paraphrased into existence
|
|
at ingest time, which is a correctness problem, not a formatting one.
|
|
Never "fix" this by editing the quote to match the source — re-read the
|
|
source, decide what it actually supports, and rewrite the page's claim.
|
|
- **`## Crux` has no quoted lines** — prose sitting under an evidence
|
|
heading. Either quote the source properly or delete the section; a
|
|
paraphrase labelled as evidence is worse than no evidence.
|
|
- **Quote is not attributed** — no `— \`path\`` line saying which source it
|
|
came from, so it can't be verified by anyone. Attribute it from the
|
|
page's `## Sources` if the origin is unambiguous, otherwise flag it.
|
|
- **`## Crux` with no `## Sources`** — an evidence section with nothing to
|
|
verify against.
|
|
|
|
Quotes under 24 characters and sources in non-text formats (PDF, DOCX,
|
|
audio) are fingerprinted but not quote-matched; the script skips them
|
|
rather than reporting false positives.
|
|
|
|
### 14 — Protected `## Notes`
|
|
|
|
This one has no script check, because it is a rule about *writing*, not a
|
|
property of a tree: no skill may rewrite, reflow, summarize, or drop a
|
|
`## Notes` section (page schema, `CLAUDE.md`/`AGENTS.md`). It is listed
|
|
here so the rule has somewhere to be enforced from. When a lint fix touches
|
|
a page — a backlink, an index entry, an archival move — carry its
|
|
`## Notes` across byte-for-byte, and check after any bulk edit that none
|
|
were lost. On a generated page under `libs/<name>/`, a missing `## Notes`
|
|
is worth adding empty so the affordance exists; anywhere else its absence
|
|
is normal and not a finding.
|
|
|
|
### Auto-fix vs. report
|
|
|
|
Auto-fix what can be done safely and mechanically: broken links, missing
|
|
backlinks, stale flags, missing index entries, duplicate log entries,
|
|
dangling graph edges, and a missing fingerprint on an otherwise-unflagged
|
|
page. Report anything that needs a judgment call
|
|
(supersession decisions, low-confidence content, ambiguous orphans) rather
|
|
than guessing on the user's behalf.
|
|
|
|
### Final step — Remind to review and sync
|
|
|
|
This is always the last step, every time this skill makes any change at
|
|
all to `wiki/`. Close with a short reminder — do not sync or push
|
|
anything yourself here:
|
|
|
|
> "Lint complete — see `wiki/log.md` for what changed (auto-fixed: ...;
|
|
> flagged for your review: ...). Please review the changes, and once
|
|
> you're happy with them, say 'sync changes' to commit and push them to
|
|
> `origin`."
|
|
|
|
If any connector-backed `libs/<name>/` was also touched, mention it
|
|
separately (its own `log.md`, not `wiki/log.md`) rather than folding it
|
|
into the same sentence.
|
|
|
|
If the lint pass made no changes at all (clean bill of health, nothing to
|
|
fix or flag), skip this reminder — there's nothing to review or sync.
|
|
|
|
## Edge cases
|
|
|
|
- **Wiki is empty or near-empty** — report that there's little to lint
|
|
yet; don't fabricate findings to look thorough. A `total findings: 0`
|
|
report is a valid outcome; say so plainly rather than hunting for
|
|
something to say.
|
|
- **A page's frontmatter is unparseable** (not just missing `type`, but
|
|
invalid YAML) — the script reports it under conformance and skips it in
|
|
the later checks rather than reasoning from a half-parsed page. Surface
|
|
it prominently and fix the frontmatter first; a page can't be judged
|
|
stale, orphaned, or archivable until it parses.
|
|
- **Retention sweep would archive a page that's clearly still in active
|
|
use** (e.g. linked from a very recent `workload/` entry) — flag it for
|
|
the user to confirm rather than auto-archiving; recency of use can
|
|
override a stale `last_updated`/`retention` combination.
|
|
- **Supersession is ambiguous** (two pages disagree and neither is
|
|
clearly newer/better corroborated) — report the conflict rather than
|
|
guessing which one wins.
|
|
- **Repeated run with nothing changed since the last lint** — produces the
|
|
same report each time (pass `--today` to pin the date if you need a
|
|
byte-identical one); don't invent variation just to seem active.
|
|
- **A changed fingerprint and a diverged crux quote on the same page** —
|
|
one incident, not two. The source was edited; fix the page once against
|
|
the new content and re-stamp both the digest and the quote together.
|
|
- **Every page in the wiki lacks fingerprints** — a wiki built before this
|
|
convention. Don't stamp them all in one pass; that produces a tree of
|
|
digests attesting to nothing anyone verified. Report the count, and
|
|
backfill as pages are touched for other reasons.
|
|
- **The script errors out or isn't runnable** (no Python 3, unreadable
|
|
tree) — say so and fall back to working the numbered checks by hand
|
|
rather than reporting a clean bill of health you didn't actually verify.
|
|
|
|
---
|
|
|
|
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../LICENSE)
|
|
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*
|