From 474630e2bb88c3909cddae3cff36b87b17f6b11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kope=C4=87?= Date: Mon, 21 Sep 2026 22:44:26 +0200 Subject: [PATCH] Adopt seven ideas from trailhq/Graft; schema 1.4 -> 1.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../software/skills/ckb-code-map/SKILL.md | 16 +- .agents/skills/ckb-decide/SKILL.md | 35 +++- .agents/skills/ckb-export-okf/SKILL.md | 2 +- .../ckb-export-okf/scripts/export_okf.py | 2 + .agents/skills/ckb-index-external/SKILL.md | 82 +++++++- .agents/skills/ckb-ingest/SKILL.md | 104 +++++++++- .agents/skills/ckb-init/SKILL.md | 8 +- .agents/skills/ckb-lint/SKILL.md | 84 +++++++- .../skills/ckb-lint/scripts/lint_report.py | 183 +++++++++++++++++- .agents/skills/ckb-reset/SKILL.md | 7 +- .agents/skills/ckb-retrieve/SKILL.md | 45 +++++ .agents/skills/ckb-upgrade/SKILL.md | 29 +++ AGENTS.md | 46 ++++- MANUAL.md | 39 +++- MANUAL.pl.md | 41 +++- README.md | 33 +++- README.pl.md | 36 +++- VERSION | 2 +- wiki/graph/index.md | 46 ++++- wiki/index.md | 2 +- wiki/log.md | 18 +- wiki/overview.md | 18 +- workload/2026-09-21_summary.md | 55 ++++++ 23 files changed, 880 insertions(+), 53 deletions(-) create mode 100644 workload/2026-09-21_summary.md diff --git a/.agents/modules/software/skills/ckb-code-map/SKILL.md b/.agents/modules/software/skills/ckb-code-map/SKILL.md index 950b810..b76b97f 100644 --- a/.agents/modules/software/skills/ckb-code-map/SKILL.md +++ b/.agents/modules/software/skills/ckb-code-map/SKILL.md @@ -121,11 +121,25 @@ Written by `ckb-spec`; leave the section here even when empty. ## Sources `src/thing` at `a1b2c3d`, mapped YYYY-MM-DD. README, `pyproject.toml`, CI config. + +## Notes + + ``` The `commit` field and the **Mapped at** line are what make this page auditable — they let a reader and `ckb-lint` tell how far the page has drifted from the code. -Never write them from memory; take them from Step 1. +Never write them from memory; take them from Step 1. They are this page's +fingerprint, serving the same role `source_fingerprint` serves elsewhere: a +commit either still matches `HEAD` or it doesn't, which beats guessing from a +date. + +`## Notes` is **protected** (page schema, `CLAUDE.md`/`AGENTS.md`). A refresh +regenerates everything above it and carries it across byte-for-byte. This +matters more here than almost anywhere else: a code map is re-run often, and +the things worth knowing about a repo that the repo doesn't say about itself — +which build target is abandoned, which service is being decommissioned, who to +ask — have nowhere else to live. ### Step 5 — Write component pages diff --git a/.agents/skills/ckb-decide/SKILL.md b/.agents/skills/ckb-decide/SKILL.md index a65b5c8..47216d4 100644 --- a/.agents/skills/ckb-decide/SKILL.md +++ b/.agents/skills/ckb-decide/SKILL.md @@ -108,9 +108,34 @@ was chosen before the reporting requirements landed. ## Sources -Where this came from — a meeting, a thread, a `raw/archive/` file, a ticket. +- `raw/archive/2026-09-21/arch-review.md` — sha256:3f9a2c1e (checked 2026-09-21) + +## Crux + +> We're going with Postgres. The reporting requirements need window +> functions and nobody wants to maintain a second analytics store. +— `raw/archive/2026-09-21/arch-review.md`, Ana Reyes ``` +A decision record is the page type where `## Crux` earns its place most +clearly. Everything above it is this skill's reconstruction of a choice; +the Crux is the moment the call was actually made, in the words it was made +in. When someone later asks "did we really decide that, or did we just +discuss it?", a verbatim quote settles it and a summary doesn't — which is +also the distinction between `status: accepted` and `status: proposed` that +`ckb-ingest` is warned about. + +Quote the decision itself, not the surrounding debate; if the material +contains no sentence where anyone actually decides, that is strong evidence +the record should be `proposed`. Fingerprint every cited local file +(`sha256sum | cut -c1-8`) so `ckb-lint` check 12 can tell you when +the source underneath a decision changes. + +Decision pages carry no `## Notes` section. Nothing regenerates them, so +there is nothing to protect against — and an append-only record with a +freely-editable annotation block invites exactly the retroactive revision +the append-only rule exists to prevent. + ### Field reference | Field | Required | Notes | @@ -236,6 +261,14 @@ points at where the decision came from. When the answer hinges on detail beyond what the page states — exact wording, a number, who was actually in the room — follow the source rather than paraphrasing the paraphrase. +Where the page has a `## Crux`, take `ckb-retrieve` Step 6's shortcut: check +the recorded fingerprint against the cited file, and if it matches, quote the +Crux directly. If it doesn't match, the source underneath this decision has +been edited since the record was written — say so, answer from the source, +and flag it. That is worth stating plainly rather than folding into a +caveat: a decision record whose evidence has moved is the one case where +"what we decided" and "what the record says we decided" can come apart. + ### Step 4 — Answer with the metadata attached A decision answer is incomplete without **who** and **when** — lead with the diff --git a/.agents/skills/ckb-export-okf/SKILL.md b/.agents/skills/ckb-export-okf/SKILL.md index 4c196d2..b73ff4b 100644 --- a/.agents/skills/ckb-export-okf/SKILL.md +++ b/.agents/skills/ckb-export-okf/SKILL.md @@ -100,7 +100,7 @@ The script prints, and you should summarize back to the user: | `tldr` | `description` | rename | | `resource` | `resource` | passthrough | | `last_updated` | `timestamp` | passthrough as-is (bare `YYYY-MM-DD` is valid ISO 8601 — no time-of-day is fabricated) | -| `confidence`, `quality`, `retention`, `supersedes`, `superseded_by`, `freshness_window_days` | same keys | passthrough as OKF extension fields, which consumers must tolerate | +| `confidence`, `quality`, `retention`, `supersedes`, `superseded_by`, `freshness_window_days`, `source_fingerprint`, `source_checked` | same keys | passthrough as OKF extension fields, which consumers must tolerate | | `kb_schema_version` | *(dropped)* | the root `index.md`'s frontmatter is spec-limited to `okf_version`; this key has no valid home in the bundle | | *(none)* | `tags` | omitted — no source field to derive it from | diff --git a/.agents/skills/ckb-export-okf/scripts/export_okf.py b/.agents/skills/ckb-export-okf/scripts/export_okf.py index 755c2b3..39a1fb8 100644 --- a/.agents/skills/ckb-export-okf/scripts/export_okf.py +++ b/.agents/skills/ckb-export-okf/scripts/export_okf.py @@ -47,6 +47,8 @@ PASSTHROUGH_EXT = [ "supersedes", "superseded_by", "freshness_window_days", + "source_fingerprint", + "source_checked", ] # Keys with no valid home in an OKF bundle. DROPPED = {"kb_schema_version"} diff --git a/.agents/skills/ckb-index-external/SKILL.md b/.agents/skills/ckb-index-external/SKILL.md index 2efcf95..8408f38 100644 --- a/.agents/skills/ckb-index-external/SKILL.md +++ b/.agents/skills/ckb-index-external/SKILL.md @@ -127,6 +127,36 @@ stop being) a given source's admin (e.g. "make me the admin for the finance reports source" / "I don't want write access to X anymore"), never as a side effect of just running "index external sources." +## Regeneration rule: `## Notes` is never overwritten + +Every page this skill writes is *generated* — a later run rebuilds it from +the connector and the previous contents are gone. That makes this index the +one place in the KB where a human annotation is most useful and most +fragile: the person who knows that a document is superseded, or that a name +in it refers to someone who has since left, has nowhere safe to write it +down. + +`## Notes` is that place. Before rewriting any page under `libs//`, +read the existing file and lift its `## Notes` section out verbatim; after +regenerating everything above it, append it back **byte-for-byte**. Not +reflowed, not summarized, not merged into the `tldr`, not "improved" — +unchanged, including whitespace and any half-finished sentence. + +Practically: `## Notes` is the last section of every generated page, so a +rebuild is "replace everything above the `## Notes` heading". Create it empty +on a page's first write so the affordance exists before anyone needs it: + +```markdown +## Notes + + +``` + +If a page somehow has content *after* `## Notes` that this skill would +otherwise generate, preserve the whole tail rather than guessing where the +human part ends. Losing generated content costs one re-run; losing a person's +annotation costs the thing itself. + ## How to run this skill ### Step 1 — Find connector-backed libs @@ -233,9 +263,38 @@ last_updated: YYYY-MM-DD freshness_window_days: 30 # this source's refresh_interval_days, or 30 if unset — shorter than a # typical wiki page, because external sources change without notice retention: medium +source_fingerprint: sha256:3f9a2c1e # see below — what actually detects drift +source_checked: YYYY-MM-DD --- ``` +**Fingerprint every document page.** A short `freshness_window_days` is a +guess that a source *might* have moved; a fingerprint is a fact about whether +it *did*. This matters far more here than in `wiki/`: an archived file under +`raw/archive/` is effectively immutable, while a SharePoint or Drive document +is edited by other people with no notification to anyone. + +Take the fingerprint from whatever the connector gives you, in this order of +preference: + +1. A version identifier the connector already maintains — an ETag, a + `version`, a content hash, a `lastModifiedDateTime`. Record it as + `etag:` / `mtime:`. This is free and needs no content + fetch. +2. Failing that, `sha256` of the fetched content, recorded as + `sha256:`. +3. Metadata-only documents with neither: omit the field rather than + inventing one, and leave `confidence` low as Step 5 already requires. + +Set `source_checked` to the date the fingerprint was last confirmed — which +is **not** always `last_updated`. A re-index that confirms a document +unchanged advances `source_checked` and leaves `last_updated` alone; that +distinction is exactly what tells a reader "this was verified yesterday" from +"this was written six months ago and nobody has looked since". + +Give every document page a trailing `## Notes` section, created empty on +first write (see Step 7's protection rule). + ### Step 6 — Extract entities and processes mentioned within each document A document-only index tells a reader *what exists*, but not *what's known @@ -289,9 +348,11 @@ authoritative (cascade priority); this page's only job is pointing back to *these specific documents* as additional evidence, which the wiki page may not have cited yet. -Record real relationships surfaced by a document between two entities -(`uses`, `depends_on`, `caused`, `contradicts`) as edges in -`libs//graph/edges.json`, same edge vocabulary `wiki/` uses. Also add +Record real relationships surfaced by a document between two entities as +edges in `libs//graph/edges.json`, using the same closed vocabulary +`wiki/graph/index.md` defines (`part_of`, `uses`, `depends_on`, `produces`, +`configures`, `validates`, `implements`, `caused`, `contradicts`, +`supersedes`) — each verb answering the question listed there. Also add a `mentioned_in` edge from each entity to every document that discusses it — this is what makes the graph a genuine index into the source documents rather than just a bag of loose pages. @@ -392,8 +453,19 @@ was read-only with nothing new to fetch), skip this reminder. user from seeing the last-known index. - **Re-running against an already-indexed source** — refresh existing document *and* entity/process pages in place (update - `tldr`/`last_updated`/`confidence`, add newly-seen `mentioned_in` edges); - never duplicate a document's or entity's page. + `tldr`/`last_updated`/`confidence`/`source_fingerprint`/`source_checked`, + add newly-seen `mentioned_in` edges); never duplicate a document's or + entity's page, and carry every `## Notes` section across untouched. +- **A document's fingerprint is unchanged since the last run** — don't + re-summarize it. Advance `source_checked` to today, leave `last_updated`, + `tldr`, and `confidence` exactly as they were, and spend the run's budget + on the documents that did change. On a large source this is most of them, + and it is the difference between a re-index that costs a full pass and one + that costs a listing. +- **A document's fingerprint changed** — re-fetch and re-summarize it, and + say so in the report. A changed fingerprint on a document some wiki page + cites is worth calling out by name: that wiki page was built from a version + of this document that no longer exists. - **A previously-indexed document is no longer found at the source** — don't delete its page. Update its `tldr` with a note ("No longer found at source as of YYYY-MM-DD") and set `retention: low`, so the existing diff --git a/.agents/skills/ckb-ingest/SKILL.md b/.agents/skills/ckb-ingest/SKILL.md index 9b5df4c..308975b 100644 --- a/.agents/skills/ckb-ingest/SKILL.md +++ b/.agents/skills/ckb-ingest/SKILL.md @@ -18,7 +18,7 @@ This skill only ever writes to `wiki/` (and moves processed files within `raw/`). It never touches `linked/` or `libs/` — those are immutable upstream sources of truth — and it never touches this repo's own git remote; pushing the resulting changes is a separate, explicit step (see -Step 7). +Step 8). ## Trigger phrases @@ -60,8 +60,12 @@ than treated as final. Identify typed entities in the source material — people, projects, libraries, concepts, systems. Create entity pages at `wiki/entities/.md` if they don't already exist. Record typed -relationships between entities — `uses`, `depends_on`, `caused`, -`contradicts`, `supersedes` — as edge data in `wiki/graph/edges.json`. +relationships between entities as edge data in `wiki/graph/edges.json`, +using the closed vocabulary in `wiki/graph/index.md` — `part_of`, `uses`, +`depends_on`, `produces`, `configures`, `validates`, `implements`, `caused`, +`contradicts`, `supersedes`. Each verb is defined there by the question it +answers; if the relationship you have in mind doesn't answer one of those +questions, it belongs in the page's prose, not in the graph. Also record `has_expertise_in` and `owns` edges when the material actually evidences them: `has_expertise_in` when a person demonstrably answers @@ -95,7 +99,35 @@ wiki/entity/source paths, exclusions, and refresh hints. Create one only when the source material shows a real repeated scope; don't manufacture scopes for one-off facts. -### Step 4 — Synthesize pages +### Step 4 — Blast radius: what does this material invalidate? + +Ingest is additive by habit, which is how a wiki accumulates two pages that +quietly disagree. Before writing anything, find out what the incoming +material lands on. + +For every entity from Step 3 that already has a page, walk +`wiki/graph/edges.json` **backwards** — every edge whose `to` is that page — +one hop out. That set, plus the entity pages themselves, is the blast radius: +the pages whose claims could be affected by what just arrived. Read their +`tldr`s (and the bodies of any that look genuinely exposed) and sort each one +into: + +- **Confirms** — the new material agrees. Note it; corroboration is a reason + to raise `confidence` in Step 5, not to rewrite anything. +- **Extends** — the new material adds detail the page doesn't have. Update + the page in Step 5. +- **Contradicts** — the new material and the page can't both be right. This + is the case worth catching: handle it as supersession (see Edge cases), and + say so explicitly in the report rather than letting the newer page silently + win. +- **Untouched** — most of the radius. Drop it and move on. + +Name the owners too. Where an `owns` or `has_expertise_in` edge points at an +affected page, that person is who should review the change — surface them in +the Step 8 report. Skip this step entirely when Step 3 produced no entity +that already exists; a first ingest into an empty wiki has no radius. + +### Step 5 — Synthesize pages Convert the core knowledge into clean, modular Markdown files. Every page gets frontmatter with: @@ -110,6 +142,53 @@ gets frontmatter with: `CLAUDE.md`/`AGENTS.md` page schema) — set it once, based on the entity/content kind (person, project, concept, library, decision, playbook, ...). +Every synthesized page ends with the reserved body sections from the page +schema. `## Sources` and `## Crux` are written here; `## Notes` is created +empty and never touched again: + +```markdown +## Sources + +- `raw/archive/2026-09-21/kickoff-notes.md` — sha256:3f9a2c1e (checked 2026-09-21) +- `raw/archive/2026-09-21/access-thread.eml` — sha256:be40d7aa (checked 2026-09-21) + +## Crux + +> FDEs need the VDI *and* a Jira account before day one; the VDI request +> alone takes ten working days. +— `raw/archive/2026-09-21/kickoff-notes.md`, under "Access" + +## Notes + + +``` + +Compute each fingerprint from the archived file, not from memory: + +```bash +sha256sum raw/archive// | cut -c1-8 +``` + +Record the digest the page was actually built from. That is what lets +`ckb-lint` tell "old but still accurate" apart from "the source changed +underneath this page" without asking a model — a calendar window can't +distinguish those. When a page is built from exactly one source, also set +`source_fingerprint` and `source_checked` in frontmatter. + +**`## Crux` is quoted, never paraphrased.** Lift the handful of lines that +actually carry the claim — the number, the constraint, the condition, the +commitment — verbatim, and attribute each quote to the source bullet it came +from. Three to ten lines is the working range; a Crux approaching the length +of the summary has stopped being evidence and become a second copy of the +source. The synthesis above it says what the material means; the Crux is what +it said, so a reader can check the first against the second. This is also +what lets `ckb-retrieve` ground an answer without opening the archive every +time. + +A page with no quotable source — synthesized from conversation, or from +material too diffuse to quote — simply has no `## Crux`. An empty or +paraphrased one is worse than none, because it looks like evidence. + For long conversations, meeting notes, transcripts, or chat exports, use a structured distillation before writing the final page: - `Question` or searchable problem statement, when there is one. @@ -145,14 +224,14 @@ thread question or section heading it sat under. An excerpt that reads unambiguously on its own is the entire point; one that needs the surrounding transcript to make sense hasn't been extracted, only moved. -### Step 5 — Link and cross-reference +### Step 6 — Link and cross-reference Use **both** `[[Wikilinks]]` (Obsidian-compatible) and standard `[markdown](path.md)` links on every cross-reference, so the wiki works in Obsidian, GitHub, and CLI tools alike. Where useful, reference upstream files directly at `linked//...` or `libs//...`. -### Step 6 — Update index and log +### Step 7 — Update index and log Add new pages to the routing table in `wiki/index.md` with a **Use when** description. If the page lives in a subdirectory, also add it to that @@ -170,10 +249,15 @@ also update `wiki/projects/index.md`. If ingest closes a previously recorded question in `wiki/query-gaps.md`, move that entry from Open to Resolved and mention the page or source that now answers it. -### Step 7 — Remind to review and sync +### Step 8 — Report, then remind to review and sync -This is always the last step, every time this skill runs and made any -change at all. Close with a short reminder — do not sync or push +Report what changed before the reminder: pages created vs. updated, and — +from Step 4 — which existing pages the new material confirmed, extended, or +contradicted, naming the owner of each contradicted page. A contradiction +resolved silently is the one outcome of an ingest a reviewer must not have to +discover for themselves. + +Then close with a short reminder — do not sync or push anything yourself here, and do not skip this even if the changes look small or obviously correct: @@ -187,7 +271,7 @@ process), skip this reminder — there's nothing to review or sync. ## Edge cases - **Inbox and `raw/` both empty** — report that there's nothing to - ingest. Don't touch `wiki/`, and skip the Step 7 reminder. + ingest. Don't touch `wiki/`, and skip the Step 8 reminder. - **Item in inbox doesn't yield a clear entity or page** (too vague, pure scratch note with no durable fact) — don't force a page into existence. Still move the item to `raw/archive//` since it's been diff --git a/.agents/skills/ckb-init/SKILL.md b/.agents/skills/ckb-init/SKILL.md index 4bd424c..cc470ad 100644 --- a/.agents/skills/ckb-init/SKILL.md +++ b/.agents/skills/ckb-init/SKILL.md @@ -249,9 +249,11 @@ generic structure: - **`wiki/error-book.md`** - copy verbatim (already generic: empty table + placeholder note). Set `last_updated` to today. - **`wiki/entities/index.md`** - header + placeholder note, no entries. -- **`wiki/graph/index.md`** - header + pointer to `edges.json`, with a - generic "Current graph coverage: (none yet)" line instead of this - repo's specific bullet list. +- **`wiki/graph/index.md`** - header + pointer to `edges.json`, the edge + vocabulary table and its conventions **verbatim** (`ckb-ingest`, + `ckb-decide`, `ckb-index-external`, and `ckb-lint` all write against that + closed set, so it is contract, not example content), and a generic + "(No edges recorded yet)" placeholder instead of any real coverage line. - **`wiki/graph/edges.json`** - `{"version": 1, "last_updated": "", "edges": []}`. - **`wiki/projects/index.md`** - header + placeholder note, no project scopes yet. diff --git a/.agents/skills/ckb-lint/SKILL.md b/.agents/skills/ckb-lint/SKILL.md index c6a4833..1fb24bc 100644 --- a/.agents/skills/ckb-lint/SKILL.md +++ b/.agents/skills/ckb-lint/SKILL.md @@ -77,13 +77,15 @@ python3 "/scripts/lint_report.py" Resolve `` 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. Exit code is `0` when nothing was found and `1` +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, and 10 below — plus the structural half of check 5 (decision -records) — across `wiki/` and every connector-backed +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//` (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. @@ -221,11 +223,78 @@ For the `software` module specifically: 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//`, 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. Report anything that needs a judgment call +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. @@ -268,6 +337,13 @@ fix or flag), skip this reminder — there's nothing to review or sync. - **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. diff --git a/.agents/skills/ckb-lint/scripts/lint_report.py b/.agents/skills/ckb-lint/scripts/lint_report.py index 0fe4ad7..cac0111 100644 --- a/.agents/skills/ckb-lint/scripts/lint_report.py +++ b/.agents/skills/ckb-lint/scripts/lint_report.py @@ -15,14 +15,15 @@ """Deterministic half of the ckb-lint health check: detect, never fix. Usage: - python3 lint_report.py [--scope wiki|libs|all] [--today YYYY-MM-DD] [--json] + python3 lint_report.py [--scope wiki|libs|all] [--today YYYY-MM-DD] [--json] [--quick] Run from the repository root (the directory containing wiki/ and libs/). This script is strictly READ-ONLY. It writes no files, moves nothing, and runs no git commands — it prints findings for the agent to act on. The mechanical checks (conformance, freshness, retention/decay candidates, orphans, graph -consistency, index/log consistency, source.yaml validity) live here; the +consistency, index/log consistency, source.yaml validity, source fingerprints, +crux verbatimness) live here; the judgment calls the ckb-lint skill owns (whether two pages genuinely supersede one another, ambiguous orphans, error-book entries, auto-fix vs. report) stay with the model. Decision records get their own structural checks here — status @@ -32,6 +33,7 @@ mechanical; deciding that a new decision *replaces* an old one is not. Exit code is 0 when nothing was found and 1 when there are findings. """ import argparse +import hashlib import json import re import sys @@ -136,6 +138,12 @@ def as_date(value): LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)") WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]") +SOURCE_BULLET_RE = re.compile(r"^\s*[-*]\s+`([^`]+)`(.*)$", re.M) +FINGERPRINT_RE = re.compile(r"\b(sha256|etag|mtime):([^\s,)]+)") +ATTRIBUTION_RE = re.compile(r"^\s*[\u2014-]\s*`([^`]+)`", re.M) +# Text-ish sources a quote can actually be checked against byte-for-byte. +# Anything else (pdf, docx, audio) is fingerprinted but never quote-verified. +QUOTABLE_SUFFIXES = {".md", ".txt", ".eml", ".csv", ".json", ".yaml", ".yml", ".html", ".rst", ".log"} class Doc: @@ -521,6 +529,124 @@ def check_sources(today): # driver # -------------------------------------------------------------------------- + +def sections(body): + """Split a page body into {heading: text} for `##`-level headings.""" + out, current, buf = {}, None, [] + for line in body.split("\n"): + match = re.match(r"^##\s+(.+?)\s*$", line) + if match: + if current is not None: + out[current] = "\n".join(buf) + current, buf = match.group(1).strip(), [] + elif current is not None: + buf.append(line) + if current is not None: + out[current] = "\n".join(buf) + return out + + +def digest(path): + """First 8 hex chars of the file's sha256, or None if unreadable.""" + try: + return hashlib.sha256(path.read_bytes()).hexdigest()[:8] + except OSError: + return None + + +def source_bullets(body): + """[(cited path, kind, value)] from a page's `## Sources` section.""" + section = sections(body).get("Sources") + if section is None: + return None + out = [] + for cited, rest in SOURCE_BULLET_RE.findall(section): + fp = FINGERPRINT_RE.search(rest) + out.append((cited.strip(), fp.group(1) if fp else None, fp.group(2) if fp else None)) + return out + + +def check_evidence(docs, root): + """Check 12 — `## Sources` fingerprints against the files they cite.""" + findings = [] + for doc in docs: + if doc.reserved or doc.fm_error: + continue + bullets = source_bullets(doc.body) + if bullets is None: + continue + if not bullets: + findings.append(f"{doc.rel}: `## Sources` section is empty") + continue + for cited, kind, value in bullets: + if re.match(r"^[a-z][a-z0-9+.-]*://", cited): + continue # a URL: nothing local to hash + target = (REPO_ROOT / cited.lstrip("/")).resolve() + if not target.is_file(): + target = (root / cited.lstrip("/")).resolve() + if not target.is_file(): + findings.append(f"{doc.rel}: cited source `{cited}` does not exist") + continue + if kind is None: + findings.append(f"{doc.rel}: cited source `{cited}` has no fingerprint recorded") + continue + if kind != "sha256": + continue # etag/mtime come from a connector; nothing local to recompute + actual = digest(target) + if actual is None: + findings.append(f"{doc.rel}: cited source `{cited}` could not be read") + elif not value.lower().startswith(actual): + findings.append( + f"{doc.rel}: source `{cited}` CHANGED since this page was built " + f"(recorded sha256:{value}, now sha256:{actual})" + ) + return findings + + +def check_crux(docs, root): + """Check 13 — `## Crux` quotes are verbatim, attributed, and non-empty.""" + findings = [] + for doc in docs: + if doc.reserved or doc.fm_error: + continue + secs = sections(doc.body) + crux = secs.get("Crux") + if crux is None: + continue + quotes = [line[1:].strip() for line in crux.split("\n") if line.startswith(">")] + quoted = " ".join(q for q in quotes if q) + if not quoted: + findings.append(f"{doc.rel}: `## Crux` has no quoted lines (evidence sections must quote, not paraphrase)") + continue + if "Sources" not in secs: + findings.append(f"{doc.rel}: `## Crux` present but the page has no `## Sources` to attribute it to") + cited = ATTRIBUTION_RE.findall(crux) + if not cited: + findings.append(f"{doc.rel}: `## Crux` quote is not attributed to a source") + continue + for ref in cited: + ref = ref.strip().lstrip("/") + target = (REPO_ROOT / ref).resolve() + if not target.is_file(): + target = (root / ref).resolve() + if not target.is_file() or target.suffix.lower() not in QUOTABLE_SUFFIXES: + continue + try: + haystack = " ".join(target.read_text(encoding="utf-8", errors="replace").split()) + except OSError: + continue + for quote in quotes: + if not quote or len(quote) < 24: + continue # too short to match meaningfully + needle = " ".join(quote.split()) + if needle not in haystack: + findings.append( + f"{doc.rel}: `## Crux` quote not found verbatim in `{ref}` — " + f"{needle[:60]!r}..." + ) + return findings + + CHECKS = [ ("1 conformance", "conformance"), ("2 freshness", "freshness"), @@ -530,8 +656,15 @@ CHECKS = [ ("6 orphans", "orphans"), ("7 graph", "graph"), ("8 index/log", "index_log"), + ("12 source fingerprints", "evidence"), + ("13 crux", "crux"), ] +# The subset worth running at session start (Rule E): cheap, and each finding +# means something has actually changed rather than merely aged on a calendar. +QUICK_CHECKS = [("conformance", "malformed"), ("freshness", "past freshness window"), + ("evidence", "source drift"), ("crux", "crux quotes diverged")] + def run_tree(root, today, label): docs = read_tree(root) @@ -546,14 +679,46 @@ def run_tree(root, today, label): "orphans": check_orphans(docs), "graph": check_graph(root, docs), "index_log": check_index_and_logs(root, docs), + "evidence": check_evidence(docs, root), + "crux": check_crux(docs, root), + "hubs": in_degree(root, docs), } +def in_degree(root, docs): + """Top pages by inbound edge count — not a finding, a retrieval signal. + + `ckb-retrieve` fuses in-degree as one ranked list among several; surfacing + it here means the same number comes from one place rather than being + recomputed by eye at query time. + """ + edges_path = root / "graph" / "edges.json" + if not edges_path.is_file(): + return [] + try: + data = json.loads(edges_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return [] + edges = data.get("edges", data) if isinstance(data, dict) else data + if not isinstance(edges, list): + return [] + counts = {} + for edge in edges: + if not isinstance(edge, dict): + continue + target = str(edge.get("to", "")).strip() + if target: + counts[target] = counts.get(target, 0) + 1 + return sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:10] + + def main(): parser = argparse.ArgumentParser(description="Read-only mechanical checks for ckb-lint.") parser.add_argument("--scope", choices=("wiki", "libs", "all"), default="all") parser.add_argument("--today", help="override today's date (YYYY-MM-DD) for reproducible runs") parser.add_argument("--json", action="store_true", help="emit machine-readable JSON instead of text") + parser.add_argument("--quick", action="store_true", + help="one-line session-start summary (Rule E): conformance, freshness, and source drift only") args = parser.parse_args() today = as_date(args.today) if args.today else date.today() @@ -582,6 +747,15 @@ def main(): total = sum(len(t[key]) for t in trees for _, key in CHECKS) + len(source_findings) + if args.quick: + counts = {key: sum(len(t[key]) for t in trees) for key, _ in QUICK_CHECKS} + flagged = [f"{counts[key]} {label}" for key, label in QUICK_CHECKS if counts[key]] + if not flagged: + print("ckb check: clean") + return 0 + print("ckb check: " + ", ".join(flagged) + " — run \"lint\" for detail") + return 1 + if args.json: print(json.dumps({"today": today.isoformat(), "trees": trees, "sources": source_findings, "findings": total}, indent=2)) return 1 if total else 0 @@ -597,6 +771,9 @@ def main(): print(f" - {item}") if not any(tree[key] for _, key in CHECKS): print(" clean") + if tree["hubs"]: + top = ", ".join(f"{name} ({n})" for name, n in tree["hubs"][:5]) + print(f" [in-degree] most-referenced pages: {top}") if args.scope in ("libs", "all"): print("=== external sources (libs/*/source.yaml)") if source_findings: @@ -607,6 +784,8 @@ def main(): print(f"total findings: {total}") print("checks NOT covered here (model's job): whether two pages genuinely supersede " "each other, 9 error-book, auto-fix vs. report") + print("note: a changed source fingerprint (check 12) or a diverged crux quote (check 13) " + "means the evidence moved, not that a page merely aged — read those first") return 1 if total else 0 diff --git a/.agents/skills/ckb-reset/SKILL.md b/.agents/skills/ckb-reset/SKILL.md index 5125a29..3e4daab 100644 --- a/.agents/skills/ckb-reset/SKILL.md +++ b/.agents/skills/ckb-reset/SKILL.md @@ -131,8 +131,11 @@ Step 8 defines it** — that skill is the single source of truth for what a clea - `wiki/overview.md` — the generic version, `last_updated` today. - `wiki/log.md`, `wiki/decisions/log.md` — headers and explanation, no entries. - `wiki/error-book.md`, `wiki/query-gaps.md` — headers, no entries. -- `wiki/entities/index.md`, `wiki/projects/index.md`, `wiki/graph/index.md` — - headers plus placeholder notes. +- `wiki/entities/index.md`, `wiki/projects/index.md` — headers plus + placeholder notes. +- `wiki/graph/index.md` — header, placeholder, and the edge-vocabulary table + and conventions **verbatim**; that table is the contract every writing + skill validates against, not accumulated content, so a reset keeps it. - `wiki/decisions/index.md` — header, placeholder, and the status-vocabulary table **verbatim** (`ckb-decide` and `ckb-lint` both validate against it). - `wiki/graph/edges.json` — `{"version": 1, "last_updated": "", "edges": []}`. diff --git a/.agents/skills/ckb-retrieve/SKILL.md b/.agents/skills/ckb-retrieve/SKILL.md index 997c5d2..f0b934a 100644 --- a/.agents/skills/ckb-retrieve/SKILL.md +++ b/.agents/skills/ckb-retrieve/SKILL.md @@ -126,6 +126,24 @@ material actually shows answering questions on that topic. Say which of the two grounded the answer, since an inferred expert is a weaker claim than a recorded one. +While the edge file is open, build one more ranked list for Step 4: +**in-degree** — for each candidate already on the shortlist, how many edges +point *at* it. + +```bash +python3 -c "import json,collections;e=json.load(open('wiki/graph/edges.json'))['edges'];\ +c=collections.Counter(x['to'] for x in e);print(c.most_common(20))" +``` + +Centrality is a genuinely different signal from every other one in Step 4: +text match, freshness, confidence and quality are all properties of a page in +isolation, while in-degree is what the rest of the KB has to say about it. A +page a dozen others depend on is usually the one that settles a question; +a leaf page with an identical `tldr` usually isn't. Rank only the candidates +already shortlisted — this ranks the shortlist, it doesn't widen it. Skip the +list entirely when `edges.json` is empty or every candidate has in-degree 0, +since a list with no ordering contributes nothing to the fusion. + ### Step 4 — Fuse the signals, dedupe, then rerank Signals disagree, and none of them is trustworthy alone: an exact `rg` hit @@ -139,6 +157,12 @@ it appears in, where `rank` is its 1-based position in that list. Use different lists beats one ranked first in a single list; consensus is the whole point. +Give the in-degree list a weight *below* 1.0 (around 0.5 is a reasonable +start). Centrality is a prior, not evidence — it says a page matters +generally, not that it answers this question — so it should break ties +between comparable candidates without ever promoting a page the text signals +didn't surface. + `k = 10` is deliberately smaller than the `k = 60` rank fusion is usually quoted with. 60 is tuned for retrievers returning hundreds of candidates, and against the dozen-or-so a local wiki produces it flattens every score @@ -184,6 +208,21 @@ changes the interpretation. This is the step this skill exists to enforce. For each page on the shortlist that will actually ground part of the answer: +- **A `wiki/` page with a `## Crux` covering the exact claim** — the Crux + is verbatim source text, not synthesis, so it is already the evidence + this step exists to fetch. Confirm the page's recorded fingerprint still + matches the cited source (cheap: `sha256sum | cut -c1-8` against + the digest in the `## Sources` bullet). If it matches, quote the Crux and + answer — one hash beats one full file read, and citing a quote is strictly + stronger than citing a summary. If it **differs**, the source moved under + the page: open the source, answer from it, and say the page's Crux is now + stale — that's a real `ckb-lint` finding, not a formality. If the page has + no fingerprint recorded at all, treat the Crux as unverified and fall + through to the full read below. + + This shortcut is narrow on purpose. It applies only when the quote covers + the specific claim being used — not when it is merely on-topic, and never + as a reason to skip reading a source that would qualify or contradict it. - **A `wiki/` page** — every page synthesized via `ckb-ingest` carries a `## Sources` section citing the exact `raw/archive//...` or `outputs/...` file(s) it was built from. Open the cited file(s) — or @@ -256,6 +295,12 @@ that would close the gap. If you edit `wiki/query-gaps.md`, update up) — say so plainly rather than silently falling back to the `tldr` as if it had been verified. Flag it as a likely `ckb-lint` finding (a broken `Sources` reference) rather than treating it as a dead end. +- **A page's `## Crux` quote is no longer found in the source it cites** — + the fingerprint check will usually catch this first, but where it doesn't + (no fingerprint recorded, or a source edited without the page being + rebuilt), answer from the source and report the divergence. A quote that + has drifted from its source is a stronger lint finding than a stale date, + because it is a page asserting something the evidence no longer says. - **Connector item unreachable or connector not authorized this session** — state clearly that the answer is based on the cached connector index, not a live re-check, and name which connector would diff --git a/.agents/skills/ckb-upgrade/SKILL.md b/.agents/skills/ckb-upgrade/SKILL.md index e83ad8a..309dce0 100644 --- a/.agents/skills/ckb-upgrade/SKILL.md +++ b/.agents/skills/ckb-upgrade/SKILL.md @@ -296,6 +296,35 @@ present, and never alter the page's body text: vice versa) — add the missing form. Don't invent new cross-references that weren't already there in some form. +**b2. Schema 1.4 → 1.5: evidence sections.** Schema 1.5 added the reserved +body sections (`## Sources`, `## Crux`, `## Notes`), the +`source_fingerprint`/`source_checked` frontmatter fields, and the completed +edge vocabulary in `wiki/graph/index.md`. Migrating a 1.4 wiki: + +- **`wiki/graph/index.md`** — replace its vocabulary section with the + template's table verbatim. Existing edges stay valid; 1.5 only added + verbs (`part_of`, `produces`, `configures`, `validates`, `implements`), + it removed none. +- **`## Sources` without fingerprints** — backfill a digest for every cited + file that still exists (`sha256sum | cut -c1-8`), with + `source_checked` set to today. Where the cited file is **gone**, do not + quietly drop the bullet: leave it and flag the page, because a page whose + evidence has vanished is a finding the migration surfaced, not one it + should bury. +- **`## Crux`** — do **not** manufacture one. A Crux is verbatim source + text, and inventing quotes during a migration is precisely the failure + the section exists to make impossible. Pages gain a Crux when they are + next ingested or deliberately re-grounded, and a 1.5 page with no Crux is + fully conformant. +- **`## Notes`** — add an empty one to generated pages under + `libs//` so the protected affordance exists. Elsewhere, leave it + absent. + +These are additive: a 1.4 page with none of them is valid 1.5, so this step +never blocks the version bump. Backfilling fingerprints is worth doing in +the same pass anyway, since it is mechanical and it is what makes `ckb-lint` +check 12 meaningful from that point on. + **c. Bump `wiki/index.md`'s `kb_schema_version`** to the template's current value, once every page has been checked. diff --git a/AGENTS.md b/AGENTS.md index a265b10..083a845 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,9 +96,34 @@ superseded_by: path/to/newer/page.md last_updated: YYYY-MM-DD freshness_window_days: 90 retention: high|medium|low +source_fingerprint: sha256:3f9a2c1e # digest of the source this page was built from +source_checked: YYYY-MM-DD # when that digest was last verified --- ``` +### Page Body Sections +Four body sections are reserved across the whole KB. All are optional, but +where present they mean exactly this and nothing else: + +- **`## Sources`** — where this page came from. One bullet per source, each + carrying a fingerprint so drift is detectable by machine rather than by + calendar: + `` - `raw/archive/2026-09-21/kickoff.md` — sha256:3f9a2c1e (checked 2026-09-21) `` + For a page built from exactly one source, the same digest also goes in + `source_fingerprint`/`source_checked` frontmatter. +- **`## Crux`** — verbatim excerpts from those sources, never a paraphrase. + Quote the few lines that actually carry the claim, attributed to the + specific source bullet they came from. A quote is evidence: it either + still matches the source or it doesn't, which is what makes drift + visible. Never edit a quote to read better — if it no longer matches, + that is a finding, not an edit. +- **`## Notes`** — human-authored, and **protected**. No skill may + rewrite, reflow, summarize, or drop this section; regeneration + preserves it byte-for-byte. It is the only place a person can annotate + an agent-regenerated page and expect it to survive. +- **`## Evidence`** / other sections are ordinary content with no special + handling. + Pages with `type: decision` live in `wiki/decisions/` as `NNNN-slug.md` and add `status` (`proposed`/`accepted`/`rejected`/`superseded`/`reversed`), `decided_on`, `decided_by`, and optionally `affects` and `review_on`. They are @@ -106,7 +131,7 @@ append-only: never rewrite a decision's substance to match a later change of mind — record a new decision that supersedes it, and set both `supersedes` and `superseded_by`. `ckb-decide` owns the format. -`wiki/index.md` alone also carries `kb_schema_version`, currently `"1.4"` +`wiki/index.md` alone also carries `kb_schema_version`, currently `"1.5"` for the base contract. An installed module may raise it additively by declaring `requires_kb_schema` and contributing optional types and fields; `ckb-module` applies that bump at install time. Detailed schema migration @@ -201,11 +226,22 @@ or answer, append a short note to `workload/YYYY-MM-DD_summary.md`. ### Rule E: Session Start and End At session start, read `wiki/index.md`, the latest `workload/` summary, and -run a cheap `git status` check. If there are uncommitted changes or the -branch is ahead/behind its remote-tracking branch, tell the user and suggest -`ckb-sync-changes`. +run two cheap checks: -At session end, update `workload/` and repeat the same unsynchronized-change +```bash +git status --short --branch +python3 .agents/skills/ckb-lint/scripts/lint_report.py --quick +``` + +`git status` covers unsynchronized history — if there are uncommitted +changes or the branch is ahead/behind its remote-tracking branch, say so and +suggest `ckb-sync-changes`. `--quick` covers knowledge rot: one line stating +how many pages are past their freshness window, how many cited sources no +longer match their recorded fingerprint, and how many pages are malformed. +It is deterministic and costs no model tokens. Report the line if anything +is non-zero and suggest `ckb-lint`; stay quiet when it is clean. + +At session end, update `workload/` and repeat the unsynchronized-change check. Do not fetch or push unless the user asks. ### Rule F: Demand-Driven Context diff --git a/MANUAL.md b/MANUAL.md index dfad0f3..82064f0 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -273,8 +273,29 @@ This runs a health check across the whole wiki: - connector-backed sources whose index is overdue for a refresh get flagged, with how overdue they are — useful even if you're read-only for that source, since it tells you who to chase +- **pages whose source has actually changed** get flagged — see below +- **quotes that no longer appear in the source they cite** get flagged - recurring systemic issues get written into `wiki/error-book.md` +The last two are worth understanding, because they're the difference +between "this page is old" and "this page is wrong". + +Every page records a fingerprint of the material it was built from. Staleness +by date is a guess: a page written a year ago may still be perfectly accurate. +A fingerprint is not a guess — the agent recomputes it and either the source +is byte-for-byte what the page was written against, or somebody edited it. +When a source changes, the page built on it goes to the top of the list, +ahead of anything that has merely aged. + +Pages also quote their sources directly, in a `## Crux` section — a few +verbatim lines carrying the actual claim, underneath the agent's summary of +it. Two things follow from that. When you ask a question, the agent can often +answer from the quote instead of re-reading the whole source, and show you the +words rather than its paraphrase of them. And when a quote stops matching its +source, that's a page asserting, in quotation marks, something its evidence no +longer says — the strongest finding lint produces, and the agent will never +"fix" it by quietly editing the quote to match. + The detection half runs as a read-only Python script (`scripts/lint_report.py`), so the same wiki always produces the same findings list — the agent reads that report and then does the parts that @@ -692,6 +713,22 @@ graph) — not that you're forbidden from touching it. If you hand-edit a wiki page, it's good practice to run "Lint" afterward so the index/log/ graph stay in sync with what you changed. +There is one exception that runs the other way. On any page the agent +*regenerates* — a connector index, a code map — everything you write is +normally lost on the next rebuild. So every such page ends with a `## Notes` +section that no skill will ever touch: + +```markdown +## Notes + + +``` + +Write whatever you want there — that this document is obsolete, that the +person named in it has left, who to actually ask. It is carried across +rebuilds byte-for-byte. Anything you write *above* that heading on a +generated page will be overwritten. + | Location | Who normally writes it | Notes | |---|---|---| | `raw/inbox/`, loose files in `raw/` | **You, only** | The agent only reads, archives, and moves things here — it never originates content in `raw/` itself. | @@ -700,7 +737,7 @@ graph stay in sync with what you changed. | `libs//` (git-copy, no `source.yaml`) | **You** (you `git clone`) | A frozen copy of an external KB. Update it by re-pulling that repo yourself, not by hand-editing files here. The agent must never write inside it. | | `libs//source.yaml` (connector) | **You, only** | Declares the connector, location, optionally how often it should be refreshed (`refresh_interval_days:`), and optionally where a shared/pre-built index lives (`index:`). The agent reads it but never writes it — same as anything else upstream. | | `libs//source.local.yaml` (connector) | **You** (or the agent, only when you explicitly ask to become/stop being that source's admin) | Per-person, per-machine `access: write`/`read` setting — never committed, never seen by anyone else. Absent = read-only, the default. | -| `libs//{index.md,entities/,graph/,log.md}` (connector) | Agent-generated, **you can freely edit** | The agent's own index of that one connector's source, built/refreshed by "Index external sources" — but only if you have `access: write` locally; read-only users just get a fetched copy. Structurally the same deal as the main `wiki/` row below — feel free to correct an entry by hand, then run "Lint" (it now also checks connector-backed indexes, respecting the same read/write split). Scoped entirely to that connector; never blended into `wiki/`. | +| `libs//{index.md,entities/,graph/,log.md}` (connector) | Agent-generated, **you can freely edit** | The agent's own index of that one connector's source, built/refreshed by "Index external sources" — but only if you have `access: write` locally; read-only users just get a fetched copy. Structurally the same deal as the main `wiki/` row below — feel free to correct an entry by hand, then run "Lint" (it now also checks connector-backed indexes, respecting the same read/write split). Scoped entirely to that connector; never blended into `wiki/`. **Only `## Notes` survives a rebuild** — put anything you want to keep there. | | `wiki/decisions/` | Agent-generated, **edit with care** | Same as the rest of `wiki/` mechanically, but these are append-only by convention: correct a typo or a misattributed name freely, and don't rewrite a decision's context or rationale to match a later view — record a superseding decision instead, so the history survives. | | `wiki/` (pages, `index.md`, `overview.md`, `log.md`, `error-book.md`, `entities/`, `graph/`) | Agent-generated, **you can freely edit** | This is the one place the agent both writes and expects you might too. Feel free to correct a page by hand — just keep the frontmatter fields intact (or update `last_updated`), and run Lint afterward if you touched something the index/graph/log reference. | | `outputs/okf/`, `outputs/starlight/` | Agent, **fully regenerated** | Don't hand-edit — these are gitignored build artifacts, silently overwritten the next time you export. If something's wrong, fix the wiki page it came from and re-export. | diff --git a/MANUAL.pl.md b/MANUAL.pl.md index 2e13c6b..e093dd3 100644 --- a/MANUAL.pl.md +++ b/MANUAL.pl.md @@ -285,8 +285,31 @@ To uruchamia przegląd kondycji całej wiki: - konektorowe źródła, których indeks jest zaległy do odświeżenia, zostają oflagowane wraz z informacją o ile — przydatne nawet jeśli masz do tego źródła dostęp tylko do odczytu, bo mówi ci, kogo dopytać +- **strony, których źródło faktycznie się zmieniło**, zostają oflagowane — + patrz niżej +- **cytaty, których nie ma już w źródle**, na które się powołują, zostają + oflagowane - powtarzające się problemy systemowe trafiają do `wiki/error-book.md` +Te dwa ostatnie warto zrozumieć, bo to różnica między „ta strona jest stara" +a „ta strona jest błędna". + +Każda strona zapisuje skrót materiału, z którego powstała. Nieaktualność +liczona datą to przypuszczenie: strona napisana rok temu może być wciąż +całkowicie poprawna. Skrót nie jest przypuszczeniem — agent przelicza go i +albo źródło jest co do bajtu tym, przeciwko czemu stronę napisano, albo ktoś +je zmienił. Gdy źródło się zmienia, strona na nim zbudowana trafia na szczyt +listy, przed wszystko, co się jedynie zestarzało. + +Strony cytują też swoje źródła wprost, w sekcji `## Crux` — kilka dosłownych +linijek niosących właściwe twierdzenie, pod streszczeniem agenta. Wynikają z +tego dwie rzeczy. Gdy zadajesz pytanie, agent często może odpowiedzieć z +cytatu zamiast ponownie czytać całe źródło i pokazać ci słowa, a nie swoją +parafrazę. A gdy cytat przestaje zgadzać się ze źródłem, to strona twierdzi — +w cudzysłowie — coś, czego jej dowód już nie mówi. To najmocniejsze +znalezisko lintu i agent nigdy nie „naprawi" go, po cichu dopasowując cytat +do źródła. + Połowa wykrywająca działa jako skrypt Python tylko-do-odczytu (`scripts/lint_report.py`), więc ta sama wiki zawsze daje tę samą listę znalezisk — agent czyta ten raport, a potem wykonuje części wymagające @@ -729,6 +752,22 @@ Jeśli ręcznie edytujesz stronę wiki, dobrą praktyką jest uruchomienie potem „Lint”, żeby indeks/dziennik/graf pozostały spójne z tym, co zmieniłeś. +Jest jeden wyjątek działający w drugą stronę. Na każdej stronie, którą agent +*regeneruje* — indeks konektora, mapa kodu — wszystko, co napiszesz, zwykle +ginie przy następnej przebudowie. Dlatego każda taka strona kończy się sekcją +`## Notes`, której żaden skill nigdy nie tknie: + +```markdown +## Notes + + +``` + +Pisz tam, co chcesz — że ten dokument jest nieaktualny, że osoba w nim +wymieniona już nie pracuje, kogo naprawdę zapytać. Treść jest przenoszona +przez przebudowy co do bajtu. Wszystko, co napiszesz *powyżej* tego nagłówka +na stronie generowanej, zostanie nadpisane. + | Lokalizacja | Kto zwykle to zapisuje | Uwagi | |---|---|---| | `raw/inbox/`, luźne pliki w `raw/` | **Tylko ty** | Agent tylko czyta, archiwizuje i przenosi rzeczy tutaj — nigdy nie tworzy treści w `raw/` sam. | @@ -737,7 +776,7 @@ zmieniłeś. | `libs//` (kopia git, bez `source.yaml`) | **Ty** (robisz `git clone`) | Zamrożona kopia zewnętrznej KB. Aktualizujesz ją, ponownie pobierając to repozytorium samodzielnie, a nie ręcznie edytując pliki tutaj. Agent nigdy nie może zapisywać wewnątrz niej. | | `libs//source.yaml` (konektor) | **Tylko ty** | Deklaruje konektor, lokalizację, opcjonalnie jak często ma być odświeżany (`refresh_interval_days:`) i opcjonalnie gdzie znajduje się współdzielony/wcześniej zbudowany indeks (`index:`). Agent go czyta, ale nigdy nie zapisuje — tak jak wszystko inne nadrzędne. | | `libs//source.local.yaml` (konektor) | **Ty** (albo agent, tylko gdy wyraźnie poprosisz o zostanie/przestanie bycia administratorem tego źródła) | Osobiste, per-komputer ustawienie `access: write`/`read` — nigdy niecommitowane, nigdy niewidoczne dla innych. Brak = tylko do odczytu, domyślnie. | -| `libs//{index.md,entities/,graph/,log.md}` (konektor) | Generowane przez agenta, **możesz swobodnie edytować** | Własny indeks agenta dla tego jednego źródła konektora, budowany/odświeżany przez „Index external sources” — ale tylko jeśli masz lokalnie `access: write`; użytkownicy tylko-do-odczytu dostają po prostu pobraną kopię. Strukturalnie ta sama zasada jak przy wierszu `wiki/` poniżej — śmiało popraw wpis ręcznie, a potem uruchom „Lint” (teraz sprawdza też indeksy oparte na konektorach, respektując ten sam podział odczyt/zapis). Ograniczone wyłącznie do tego konektora; nigdy nie wmieszane w `wiki/`. | +| `libs//{index.md,entities/,graph/,log.md}` (konektor) | Generowane przez agenta, **możesz swobodnie edytować** | Własny indeks agenta dla tego jednego źródła konektora, budowany/odświeżany przez „Index external sources” — ale tylko jeśli masz lokalnie `access: write`; użytkownicy tylko-do-odczytu dostają po prostu pobraną kopię. Strukturalnie ta sama zasada jak przy wierszu `wiki/` poniżej — śmiało popraw wpis ręcznie, a potem uruchom „Lint” (teraz sprawdza też indeksy oparte na konektorach, respektując ten sam podział odczyt/zapis). Ograniczone wyłącznie do tego konektora; nigdy nie wmieszane w `wiki/`. **Przebudowę przetrwa tylko `## Notes`** — trzymaj tam wszystko, co chcesz zachować. | | `wiki/decisions/` | Generowane przez agenta, **edytuj ostrożnie** | Mechanicznie tak samo jak reszta `wiki/`, ale te strony są z założenia tylko do dopisywania: popraw swobodnie literówkę czy źle przypisane nazwisko, ale nie przepisuj kontekstu ani uzasadnienia decyzji pod późniejszy pogląd — zapisz zamiast tego decyzję zastępującą, żeby historia przetrwała. | | `wiki/` (strony, `index.md`, `overview.md`, `log.md`, `error-book.md`, `entities/`, `graph/`) | Generowane przez agenta, **możesz swobodnie edytować** | To jedyne miejsce, w którym zarówno agent zapisuje, jak i spodziewa się, że ty też możesz. Śmiało popraw stronę ręcznie — zachowaj tylko pola frontmatteru (lub zaktualizuj `last_updated`) i uruchom potem Lint, jeśli dotknąłeś czegoś, do czego odwołuje się indeks/graf/dziennik. | | `outputs/okf/`, `outputs/starlight/` | Agent, **w pełni regenerowane** | Nie edytuj ręcznie — to zignorowane przez git artefakty budowania, cicho nadpisywane przy każdym kolejnym eksporcie. Jeśli coś jest nie tak, popraw stronę wiki, z której to pochodzi, i wyeksportuj ponownie. | diff --git a/README.md b/README.md index 98fb2de..276e2ab 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,8 @@ superseded_by: path/to/new.md last_updated: YYYY-MM-DD freshness_window_days: 90 # Days before considered stale retention: high|medium|low +source_fingerprint: sha256:3f9a2c1e # digest of the source this page was built from +source_checked: YYYY-MM-DD # when that digest was last verified --- ``` @@ -226,17 +228,38 @@ retention: high|medium|low - **supersedes / superseded_by** — when new info replaces old, link them - **freshness_window_days** — pages older than this get flagged during lint - **retention** — low pages are archived after 2× freshness window +- **source_fingerprint / source_checked** — a digest of the material the page + was synthesized from, plus when it was last confirmed. A freshness window is + a guess that a source *might* have moved; a fingerprint is a fact about + whether it *did*, and lint checks it mechanically. -`wiki/index.md` alone also carries `kb_schema_version` (e.g. `"1.3"`), declaring +#### Reserved body sections +Four `##` headings mean something specific everywhere in the KB: + +| Section | What it holds | +|---|---| +| `## Sources` | one bullet per source, each with a fingerprint | +| `## Crux` | verbatim quotes from those sources — evidence, never paraphrase | +| `## Notes` | human-authored and **protected**: no skill rewrites it, ever | + +`## Crux` is what lets a question be answered from the page instead of from +the archive: a summary can drift silently, a quote either still matches its +source or it doesn't. `## Notes` is the inverse guarantee — on pages the agent +regenerates (connector indexes, code maps), it is the one place an annotation +survives the next rebuild. + +`wiki/index.md` alone also carries `kb_schema_version` (currently `"1.5"`), declaring which revision of this schema the wiki was authored against — bump minor for additive optional fields, major for breaking changes. ### Entity Extraction & Knowledge Graph During ingest, the agent extracts typed entities (people, projects, libraries, concepts, systems) and stores them as pages in `wiki/entities/`. Typed -relationships are recorded in `wiki/graph/edges.json` — structural ones -(`uses`, `depends_on`, `caused`, `contradicts`, `supersedes`) plus -people-to-topic ones (`has_expertise_in`, `owns`). Queries can walk the +relationships are recorded in `wiki/graph/edges.json`, using a closed +vocabulary where each verb is defined by the question it answers (see +`wiki/graph/index.md`) — structural ones (`part_of`, `uses`, `depends_on`, +`produces`, `configures`, `validates`, `implements`, `caused`, `contradicts`, +`supersedes`) plus people-to-topic ones (`has_expertise_in`, `owns`). Queries can walk the graph to discover connected pages (e.g. "what depends on Redis?") or to answer "who knows about X" directly. @@ -326,6 +349,8 @@ Periodically (or on demand), the agent health-checks the wiki: - **Orphan detection** — finds pages with no inbound links - **Graph consistency** — verifies all edges point to existing entities - **Index/log consistency** — verifies every subdirectory has an index.md and no change is double-logged +- **Source fingerprints** — recomputes each cited source's digest and flags pages whose evidence has actually changed, not merely aged +- **Crux verbatimness** — flags a quoted excerpt that no longer appears in the source it cites - **Connector cadence** — flags a connector-backed source whose generated index is overdue against its `refresh_interval_days`, and by how much - **Error Book** — records systemic issues with root cause and fix diff --git a/README.pl.md b/README.pl.md index 95f8a4d..e1a7cbc 100644 --- a/README.pl.md +++ b/README.pl.md @@ -227,6 +227,8 @@ superseded_by: path/to/new.md last_updated: YYYY-MM-DD freshness_window_days: 90 # Liczba dni, po których treść uznaje się za nieaktualną retention: high|medium|low +source_fingerprint: sha256:3f9a2c1e # skrót źródła, z którego zbudowano tę stronę +source_checked: YYYY-MM-DD # kiedy ten skrót był ostatnio zweryfikowany --- ``` @@ -244,8 +246,27 @@ retention: high|medium|low podczas lintowania - **retention** — strony o niskim priorytecie są archiwizowane po 2× oknie świeżości +- **source_fingerprint / source_checked** — skrót materiału, z którego + zsyntetyzowano stronę, oraz data ostatniego potwierdzenia. Okno świeżości to + przypuszczenie, że źródło *mogło* się zmienić; skrót to fakt, czy *się + zmieniło* — i lint sprawdza go mechanicznie. -Sam `wiki/index.md` dodatkowo zawiera `kb_schema_version` (np. `"1.3"`), +#### Zastrzeżone sekcje treści +Cztery nagłówki `##` mają w całej KB ściśle określone znaczenie: + +| Sekcja | Co zawiera | +|---|---| +| `## Sources` | po jednym punkcie na źródło, każdy ze skrótem | +| `## Crux` | dosłowne cytaty z tych źródeł — dowód, nigdy parafraza | +| `## Notes` | pisane przez człowieka i **chronione**: żaden skill ich nie nadpisuje | + +`## Crux` pozwala odpowiedzieć na pytanie ze strony zamiast z archiwum: +streszczenie może po cichu odpłynąć od źródła, cytat albo wciąż się z nim +zgadza, albo nie. `## Notes` daje odwrotną gwarancję — na stronach +regenerowanych przez agenta (indeksy konektorów, mapy kodu) to jedyne miejsce, +w którym adnotacja przetrwa kolejną przebudowę. + +Sam `wiki/index.md` dodatkowo zawiera `kb_schema_version` (obecnie `"1.5"`), deklarujący, według której wersji tego schematu wiki została napisana — zwiększaj wersję pomniejszą dla dodatkowych opcjonalnych pól, główną dla zmian łamiących kompatybilność. @@ -253,9 +274,12 @@ zmian łamiących kompatybilność. ### Ekstrakcja encji i graf wiedzy Podczas ingestu agent wydobywa typowane encje (osoby, projekty, biblioteki, koncepcje, systemy) i zapisuje je jako strony w `wiki/entities/`. Typowane -relacje są zapisywane w `wiki/graph/edges.json` — strukturalne (`uses`, -`depends_on`, `caused`, `contradicts`, `supersedes`) oraz łączące osoby z -tematami (`has_expertise_in`, `owns`). Zapytania mogą przechodzić po grafie, +relacje są zapisywane w `wiki/graph/edges.json` przy użyciu zamkniętego +słownika, w którym każdy czasownik jest zdefiniowany przez pytanie, na jakie +odpowiada (patrz `wiki/graph/index.md`) — strukturalne (`part_of`, `uses`, +`depends_on`, `produces`, `configures`, `validates`, `implements`, `caused`, +`contradicts`, `supersedes`) oraz łączące osoby z tematami +(`has_expertise_in`, `owns`). Zapytania mogą przechodzić po grafie, aby odkrywać powiązane strony (np. „co zależy od Redis?") albo bezpośrednio odpowiadać na „kto wie o X". @@ -354,6 +378,10 @@ Okresowo (lub na żądanie) agent sprawdza kondycję wiki: istniejące encje - **Spójność indeksu/dziennika** — weryfikuje, czy każdy podkatalog ma index.md i czy żadna zmiana nie jest podwójnie logowana +- **Skróty źródeł** — przelicza skrót każdego cytowanego źródła i oznacza + strony, których dowód faktycznie się zmienił, a nie tylko się zestarzał +- **Dosłowność sekcji Crux** — oznacza cytat, którego nie ma już w źródle, + na które się powołuje - **Częstotliwość konektorów** — oznacza konektorowe źródło, którego generowany indeks jest zaległy względem `refresh_interval_days`, wraz z informacją o ile diff --git a/VERSION b/VERSION index bd8bf88..27f9cd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.7.0 +1.8.0 diff --git a/wiki/graph/index.md b/wiki/graph/index.md index 2af5c5e..0b22277 100644 --- a/wiki/graph/index.md +++ b/wiki/graph/index.md @@ -2,9 +2,47 @@ Edge list and relationship data for the knowledge graph, keyed by entity page. -* `edges.json` - Typed relationships between entity pages. Created on the first ingest that extracts entities. - * Structural: `uses`, `depends_on`, `caused`, `contradicts`, `supersedes`. - * Decision edges: `decided_by` (decision page -> the person who made the call) and `affects` (decision page -> the entity, project, or system it constrains). These make "who decided X", "what decisions touch Y", and "what changed after Z" direct graph lookups. Written by `ckb-decide` when a decision is recorded. - * People-to-topic: `has_expertise_in` (demonstrated ability to answer questions on a topic) and `owns` (declared responsibility for a system, area, or decision). These make "who knows about X" and "who owns X" a direct graph lookup rather than a full-text guess. Recorded only from demonstrated evidence, never inferred from attendance or job title. +`edges.json` holds typed relationships between pages. Created on the first +ingest that extracts entities. + +## Edge vocabulary + +Each verb exists because it answers a question retrieval actually gets asked. +If a proposed edge doesn't answer one of these, it doesn't belong in the graph +— put it in prose on the page instead. + +| Verb | Question it answers | Written by | +|---|---|---| +| `part_of` | Where does this live? What is it a piece of? | `ckb-ingest`, `ckb-code-map` | +| `uses` | What does this reach for at runtime? | `ckb-ingest`, `ckb-index-external` | +| `depends_on` | What breaks if I change this? | `ckb-ingest`, `ckb-code-map` | +| `produces` | Where does this output come from? | `ckb-ingest` | +| `configures` | What changes this thing's behaviour? | `ckb-ingest` | +| `validates` | What checks, tests, or judges this? | `ckb-ingest` | +| `implements` | What contract must this honour? | `ckb-ingest`, `ckb-spec` | +| `caused` | Why did this happen? | `ckb-ingest` | +| `contradicts` | What disagrees with this, unresolved? | `ckb-ingest`, `ckb-lint` | +| `supersedes` | What replaced this, and what did it replace? | `ckb-ingest`, `ckb-decide`, `ckb-lint` | +| `decided_by` | Who made this call? | `ckb-decide` | +| `affects` | What does this decision constrain? | `ckb-decide` | +| `has_expertise_in` | Who can answer questions on this? | `ckb-ingest` | +| `owns` | Who is responsible for this? | `ckb-ingest`, `ckb-code-map` | +| `mentioned_in` | Which source document discusses this? | `ckb-index-external` (lib indexes only) | + +Conventions: + +* **One direction per relationship.** `part_of`, `supersedes`, `depends_on`, + and `uses` are canonical; don't also record the inverse (`contains`, + `superseded_by`, …) as a second edge. Frontmatter carries the inverse where + a page needs to state it (`superseded_by:`), and traversal reads edges both + ways regardless. +* **Evidence, not inference.** `has_expertise_in` and `owns` are recorded only + from demonstrated evidence — someone attending a meeting is not expertise, + and a job title is not ownership. The same applies to every other verb: an + absent edge beats a fabricated one. +* **In-degree is a retrieval signal.** How many edges point *at* a page is a + rough measure of how central it is, and `ckb-retrieve` fuses it as one + ranked list among several. That only works if edges are recorded honestly; + padding the graph degrades search rather than improving it. *(No edges recorded yet — populated on the next ingest.)* diff --git a/wiki/index.md b/wiki/index.md index 993eef1..3139486 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -1,5 +1,5 @@ --- -kb_schema_version: "1.4" +kb_schema_version: "1.5" --- # Knowledge Base Index diff --git a/wiki/log.md b/wiki/log.md index d5e5584..4e5660c 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -16,4 +16,20 @@ Each entry uses this format: Changes to pages under `wiki/decisions/` live in `wiki/decisions/log.md` instead, per the Recursive Index & Log Convention. -*(No entries yet.)* +## [2026-09-21 22:41] - UPDATE (schema 1.4 -> 1.5) +- **File Affected:** `wiki/index.md` +- **Description:** Bumped `kb_schema_version` to `"1.5"`, which adds the reserved body sections (`## Sources`, `## Crux`, `## Notes`), the `source_fingerprint`/`source_checked` frontmatter fields, and a completed edge vocabulary. Additive only — every 1.4 page remains valid. +- **Source:** Chat conversation (analysis of trailhq/Graft), branch `graft-ideas`. +--- + +## [2026-09-21 22:41] - RESTRUCTURE +- **File Affected:** `wiki/graph/index.md` +- **Description:** Replaced the prose edge list with a closed vocabulary table defining each verb by the question it answers, and added the missing `part_of` (written by `ckb-code-map` but previously undocumented) plus `produces`, `configures`, `validates`, `implements`. Added conventions on edge direction, evidence-only edges, and in-degree as a retrieval signal. +- **Source:** Chat conversation (analysis of trailhq/Graft), branch `graft-ideas`. +--- + +## [2026-09-21 22:41] - UPDATE +- **File Affected:** `wiki/overview.md` +- **Description:** Documented the two new frontmatter fields and added a "Reserved Page Sections" section covering `## Sources`, `## Crux`, and the protected `## Notes`. +- **Source:** Chat conversation (analysis of trailhq/Graft), branch `graft-ideas`. +--- diff --git a/wiki/overview.md b/wiki/overview.md index ca573b3..8cf5811 100644 --- a/wiki/overview.md +++ b/wiki/overview.md @@ -1,7 +1,7 @@ --- type: overview tldr: High-level map of the cascade knowledge base's directory structure, frontmatter schema, and layering rules. -last_updated: 2026-09-20 +last_updated: 2026-09-21 --- # Knowledge Base Overview @@ -37,11 +37,25 @@ When the same entity exists in multiple layers, the local version wins. ## Page Frontmatter Every wiki page carries YAML frontmatter with a required `type` field, plus optional `resource`, `tldr`, `confidence`, `quality`, `supersedes`, -`freshness_window_days`, and `retention`. Pages with `type: decision` add +`freshness_window_days`, `retention`, `source_fingerprint`, and +`source_checked`. Pages with `type: decision` add `status`, `decided_on`, `decided_by`, `affects`, and `review_on`. `wiki/index.md` additionally declares `kb_schema_version` for the bundle as a whole. See AGENTS.md for the full schema. +## Reserved Page Sections +Three `##` headings mean the same thing on every page in every layer: + +* **`## Sources`** — one bullet per source, each carrying a fingerprint + (`sha256:` for a local file, `etag:`/`mtime:` for a connector item) and the + date it was last checked. This is what makes drift detectable mechanically + rather than by calendar. +* **`## Crux`** — verbatim excerpts from those sources, attributed to the + bullet they came from. Evidence, never paraphrase: a quote either still + matches its source or it doesn't. +* **`## Notes`** — human-authored and **protected**. No skill rewrites, + reflows, or drops it; regeneration preserves it byte-for-byte. + ## Decision Records `wiki/decisions/` holds one numbered page per decision (`NNNN-slug.md`), recording what was decided, by whom, on what date, why, and which earlier diff --git a/workload/2026-09-21_summary.md b/workload/2026-09-21_summary.md new file mode 100644 index 0000000..7ebcabd --- /dev/null +++ b/workload/2026-09-21_summary.md @@ -0,0 +1,55 @@ +# 2026-09-21 Session Summary + +## [2026-09-21] Analysis — Graft (trailhq/Graft) vs. this KB structure + +- **Type:** read-only analysis. No `wiki/` writes, so no `wiki/log.md` entry. +- **Read:** `wiki/index.md`, `wiki/graph/index.md`, `wiki/entities/index.md`, + `.agents/skills/ckb-retrieve/SKILL.md`, `.agents/skills/ckb-lint/SKILL.md`, + `.agents/modules/software/skills/ckb-code-map/SKILL.md`, repo tree. +- **External source:** (README, fetched 2026-09-21). +- **Finding:** Graft is a *derived, disposable* code-context graph (tree-sitter pass + + optional LLM pass, gitignored, regenerated per developer); CKB is a *durable, curated* + knowledge layer over non-regenerable material. Architectures are not competitors. +- **Seven portable ideas identified**, ranked: (1) verbatim `## Crux`/evidence excerpts + in pages; (2) protected `## Notes` block on agent-regenerated pages; (3) content-hash + fingerprints on `## Sources` for mechanical staleness; (4) cheap deterministic + pre-pass + a `ckb check` freshness signal at session start; (5) graph in-degree as a + rank-fusion signal in `ckb-retrieve`; (6) blast-radius pass during ingest; + (7) complete + question-oriented edge vocabulary in `wiki/graph/index.md`. +- **Defect spotted:** `wiki/graph/index.md` documents `uses`/`depends_on`/`caused`/ + `contradicts`/`supersedes` but omits `part_of`, which `ckb-code-map` Step 6 writes. +- **Rejected as non-portable:** gitignored/regenerable store, MCP server + CLI daemon, + tree-sitter parsing, statusline/hooks, telemetry. +- **Status:** analysis only, no changes proposed to disk yet. Awaiting user decision on + which ideas to implement. +- **Git:** branch `main` clean at session start; this summary is the only new file. + +## [2026-09-21] Implementation — Graft ideas 1-7 on branch `graft-ideas` + +Branched from `main` at c998489. Schema bumped 1.4 -> 1.5 (additive; every 1.4 +page stays valid). VERSION 1.7.0 -> 1.8.0. + +| # | Idea | Where it landed | +|---|---|---| +| 1 | `## Crux` verbatim evidence | `AGENTS.md` §2, `ckb-ingest` Step 5, `ckb-decide`, `ckb-retrieve` Step 6 shortcut, lint check 13 | +| 2 | Protected `## Notes` | `AGENTS.md` §2, `ckb-index-external` regeneration rule, `ckb-code-map`, lint check 14 | +| 3 | Source fingerprints | `source_fingerprint`/`source_checked` frontmatter, `ckb-ingest`, `ckb-index-external` (etag/mtime/sha256), `ckb-decide`, lint check 12 | +| 4 | Cheap pre-pass + `ckb check` | `lint_report.py --quick`, wired into Rule E at session start; `ckb-index-external` skips unchanged docs by fingerprint | +| 5 | In-degree rank fusion | `ckb-retrieve` Step 3 builds the list, Step 4 fuses at weight ~0.5; `in_degree()` in the lint script surfaces hubs | +| 6 | Blast radius | new `ckb-ingest` Step 4 (reverse graph walk, confirms/extends/contradicts/untouched + owners), reported in Step 8 | +| 7 | Edge vocabulary | `wiki/graph/index.md` rewritten as a question-per-verb table; added `part_of` (the live inconsistency with `ckb-code-map`), `produces`, `configures`, `validates`, `implements` | + +Also updated for consistency: `ckb-init`/`ckb-reset` (graph vocabulary is now +scaffold contract, copied verbatim), `ckb-upgrade` (new step b2, the 1.4->1.5 +migration), `ckb-export-okf` (two new passthrough fields), `README.md`, +`README.pl.md`, `MANUAL.md`, `MANUAL.pl.md`, `wiki/overview.md`. + +**Verification:** new lint checks exercised against a synthetic fixture in the +scratchpad — a good page (clean), a page with a stale digest + missing file + +missing fingerprint + a fabricated quote, and a page with a paraphrased Crux. +All five findings fired, the good page produced none. Repo's own wiki lints +clean; both exporters run clean. + +**Not done:** ideas beyond 1-7 (Graft's token-budgeted `map` view was idea 8 +and out of scope). No wiki content was fingerprinted, because this wiki is an +empty scaffold — the conventions take effect on the next ingest.