Compare commits

...

2 commits

Author SHA1 Message Date
2c4d57acbc Add bilingual CHANGELOG with full schema and version history
Two version numbers exist in this project and are easy to confuse:
kb_schema_version (the content contract, in wiki/index.md) and VERSION
(the tooling layer). Until now neither had a written history, and the
page schema was documented in four places at three levels of detail.

CHANGELOG.md / CHANGELOG.pl.md consolidate both: the current schema in
full (frontmatter for all pages and for decisions, the three reserved
body sections, the closed edge vocabulary with a "since" column, the
reserved scaffold), then the schema history 1.1-1.5 and the template
history 1.0.0-1.8.0.

The history is reconstructed from git rather than from memory, so it
records what actually happened rather than a tidied version of it:

- There was never a schema 1.0; versioning began at 1.1 on 2026-07-13.
- Template 1.4.0 and 1.5.0 were never published — VERSION jumped from
  1.3.0 to 1.6.0 on 2026-09-01.
- Connector-backed libs shipped as tooling in 1.1.0, but the schema only
  recorded them at 1.2 nine days later. The schema column shows what was
  in effect after each release, with a footnote on the lag.

Registered with ckb-init (copied verbatim into a new KB) and ckb-upgrade
(taken wholesale from upstream rather than merged, since upstream is
authoritative about its own history). Cross-linked from both READMEs and
both MANUALs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 23:16:43 +02:00
474630e2bb Adopt seven ideas from trailhq/Graft; schema 1.4 -> 1.5
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>
2026-09-21 22:44:26 +02:00
25 changed files with 1604 additions and 60 deletions

View file

@ -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
<!-- Yours. Never rewritten by any skill. -->
```
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

View file

@ -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 <file> | 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

View file

@ -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 |

View file

@ -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"}

View file

@ -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/<name>/`,
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
<!-- Yours. Never rewritten by any skill. -->
```
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:<value>` / `mtime:<iso8601>`. This is free and needs no content
fetch.
2. Failing that, `sha256` of the fetched content, recorded as
`sha256:<first 8 hex chars>`.
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/<name>/graph/edges.json`, same edge vocabulary `wiki/` uses. Also add
Record real relationships surfaced by a document between two entities as
edges in `libs/<name>/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

View file

@ -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/<entity-name>.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
<!-- Yours. Never rewritten by any skill. -->
```
Compute each fingerprint from the archived file, not from memory:
```bash
sha256sum raw/archive/<date>/<file> | 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/<name>/...` or `libs/<name>/...`.
### 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/<YYYY-MM-DD>/` since it's been

View file

@ -217,6 +217,10 @@ actual project entity):
should point somewhere else - it's what lets the new KB's own
`ckb-upgrade` find template updates later.
- `MANUAL.md` and `MANUAL.pl.md` - the task-oriented user guide.
- `CHANGELOG.md` and `CHANGELOG.pl.md` - the full page schema reference plus
the schema and template version histories. Copy verbatim: the history
belongs to the template, not to the new KB, and a fresh KB starts on the
version that history ends at.
- `VERSION` - copy the exact current value; the new KB starts life on the
same template version it was just cloned from.
@ -249,9 +253,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": "<today>", "edges": []}`.
- **`wiki/projects/index.md`** - header + placeholder note, no project
scopes yet.

View file

@ -77,13 +77,15 @@ 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. 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/<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.
@ -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/<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. 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.

View file

@ -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

View file

@ -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": "<today>", "edges": []}`.

View file

@ -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 <source> | 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/<date>/...` 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

View file

@ -16,8 +16,8 @@ neither:
- **Template layer** — the schema/tooling that defines *how* the KB
behaves: `AGENTS.md`/`CLAUDE.md`, `.agents/skills/*`, `LICENSE`,
`VERSION`, `README.md`/`README.pl.md`/`MANUAL.md`/`MANUAL.pl.md`, and the
base `.gitignore` rules. Compared via the root `VERSION` file. Freely
`VERSION`, `README.md`/`README.pl.md`/`MANUAL.md`/`MANUAL.pl.md`/
`CHANGELOG.md`/`CHANGELOG.pl.md`, and the base `.gitignore` rules. Compared via the root `VERSION` file. Freely
upgradable — these files hold no project-specific content.
- **Content layer** — everything the KB has actually learned:
`wiki/entities/`, `wiki/graph/edges.json`, `wiki/index.md`'s entity rows,
@ -149,8 +149,11 @@ The template layer is exactly these paths — never anything under `wiki/`,
symlink)
- `LICENSE`
- `VERSION`
- `README.md`, `README.pl.md`, `MANUAL.md`, `MANUAL.pl.md` — whichever
exist upstream (an older local KB may not have some of these yet)
- `README.md`, `README.pl.md`, `MANUAL.md`, `MANUAL.pl.md`,
`CHANGELOG.md`, `CHANGELOG.pl.md` — whichever exist upstream (an older
local KB may not have some of these yet). Take the changelogs from
upstream wholesale rather than merging: they record the template's
history, which the upstream copy is authoritative about.
- `.agents/skills/<name>/**` for every skill folder present in
`tmp/ckb-upgrade-src/.agents/skills/`
- `.agents/modules/<name>/**` for every optional module present upstream
@ -296,6 +299,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 <file> | 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/<name>/` 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.

View file

@ -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

334
CHANGELOG.md Normal file
View file

@ -0,0 +1,334 @@
# Changelog & Schema Reference
*Read this in: **English** | [Polski](CHANGELOG.pl.md)*
This file tracks two things: the page schema exactly as it stands today, and
how this project arrived at its current version numbers.
There are **two independent version numbers**, and they are not the same
thing:
| Number | Lives in | Describes | Who bumps it |
|---|---|---|---|
| `kb_schema_version` | frontmatter of `wiki/index.md` | the **content contract** — what a page may contain and what those fields mean | `ckb-upgrade` (on a confirmed migration), `ckb-module` (additively, on install) |
| Template version | [`VERSION`](VERSION) | the **tooling layer**`AGENTS.md`, the skills, the scripts, the docs | `ckb-upgrade`, when it pulls a newer template |
They move independently on purpose. You can take a newer set of skills
without touching a single wiki page, and a wiki authored against an older
schema keeps working — that is what the schema version is for.
**Versioning policy.** Bump the **minor** version for an additive change: a
new optional field, a new optional body section, a new optional scaffold
file. Bump the **major** version for a breaking one: changing or removing a
required field, or changing an existing reserved filename convention. There
has never been a major bump; every schema version to date has been additive,
so any page written since 2026-07-13 is still valid today.
---
## Contents
1. [Current page schema (1.5)](#current-page-schema-kb_schema_version-15)
2. [KB schema version history](#kb-schema-version-history)
3. [Template version history](#template-version-history)
4. [Migrating between versions](#migrating-between-versions)
---
## Current page schema (`kb_schema_version: "1.5"`)
### Frontmatter — every non-reserved page
Every `.md` file under `wiki/` except the reserved ones (`index.md`,
`log.md`) carries YAML frontmatter. Only `type` is required; the rest are
optional, and preferred where meaningful. The parser is deliberately simple —
flat `key: value` pairs only, no nesting.
| Field | Required | Meaning |
|---|---|---|
| `type` | **yes** | Open string: `person`, `project`, `concept`, `library`, `decision`, `playbook`, `repository`, `component`, … New values are always valid; readers tolerate unrecognised ones. |
| `resource` | no | Canonical URI of the authoritative external source this page describes, kept separate from the wiki's own commentary. |
| `tldr` | no | One-sentence summary optimised for LLM reading. This is what the index shows and what decides whether a page gets opened at all. |
| `confidence` | no | `0.0``1.0`. Source corroboration. Set on write, decays if nothing reinforces it, raised by a new source that agrees. |
| `quality` | no | `0.0``1.0`. Self-evaluation of the page's own structure and citations. Below `0.7` is flagged for review. |
| `supersedes` | no | Path to the older page this one replaces. |
| `superseded_by` | no | Path to the newer page that replaced this one. **Always set both sides.** |
| `last_updated` | no | `YYYY-MM-DD`. When the *page* last changed. |
| `freshness_window_days` | no | Days before lint flags the page as stale. Typical: 90 for a wiki page, 365 for a decision, 30 for a connector-indexed document. |
| `retention` | no | `high` / `medium` / `low`. A `low` page is archived (never deleted) after 2× its freshness window. |
| `source_fingerprint` | no | *(1.5)* Digest of the source this page was built from — `sha256:<8 hex>` for a local file, or `etag:<value>` / `mtime:<iso8601>` for a connector item. |
| `source_checked` | no | *(1.5)* `YYYY-MM-DD` — when that digest was last verified. Distinct from `last_updated`: a re-check that finds nothing changed advances this and leaves `last_updated` alone. |
`wiki/index.md` alone carries `kb_schema_version`. It is a bundle-level
declaration, not a per-page one — individual pages never carry it.
### Frontmatter — decision pages
Pages with `type: decision` live in `wiki/decisions/` as `NNNN-slug.md` and
add:
| Field | Required | Meaning |
|---|---|---|
| `status` | **yes** | `proposed` / `accepted` / `rejected` / `superseded` / `reversed`. The vocabulary is defined in `wiki/decisions/index.md` and validated by lint. |
| `decided_on` | for `accepted`/`rejected`/`reversed` | `YYYY-MM-DD`, the date the call was made. |
| `decided_by` | when known | Comma-separated names. Where genuinely unknown, write `unknown` rather than omitting the field — "we don't know who decided this" is itself worth recording. |
| `affects` | no | Comma-separated wiki paths this decision constrains. |
| `review_on` | no | `YYYY-MM-DD` to revisit. Lint reports these once the date passes. |
Decision records are **append-only**. A decision is never rewritten to match
a later change of mind: record a new one that supersedes it, and both stay on
the record.
### Reserved body sections
*(New in 1.5.)* Four `##` headings mean the same thing on every page in every
layer of the cascade. All are optional; where present, they mean exactly this
and nothing else.
#### `## Sources`
Where the page came from. One bullet per source, each carrying a fingerprint:
```markdown
## Sources
- `raw/archive/2026-09-21/kickoff-notes.md` — sha256:3f9a2c1e (checked 2026-09-21)
```
A freshness window is a guess that a source *might* have moved. A fingerprint
is a fact about whether it *did*. Lint recomputes local digests and flags what
actually changed, which is a different and more urgent finding than a page
that has merely aged.
#### `## Crux`
Verbatim excerpts from those sources — evidence, never paraphrase — attributed
to the source bullet they came from:
```markdown
## 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"
```
Three to ten lines is the working range. A Crux approaching the length of the
summary above it has stopped being evidence and become a second copy of the
source. Two things follow from quoting rather than paraphrasing: a question
can often be answered from the page instead of the archive, and drift becomes
visible — a summary can wander from its source silently, a quote either still
matches or it does not.
A page with no quotable source simply has no `## Crux`. An empty or
paraphrased one is worse than none, because it looks like evidence.
#### `## Notes`
Human-authored, and **protected**. No skill may rewrite, reflow, summarise or
drop this section; regeneration preserves it byte-for-byte.
```markdown
## Notes
<!-- Yours. Never rewritten by any skill. -->
```
This matters most on pages the agent *regenerates* — connector indexes, code
maps — where everything else is discarded and rebuilt on the next run. It is
the only place an annotation survives. Decision pages deliberately have no
`## Notes`: nothing regenerates them, and an append-only record with a freely
editable annotation block invites exactly the retroactive revision the
append-only rule exists to prevent.
### Edge vocabulary
Relationships live in `wiki/graph/edges.json` (and each connector index's own
`graph/edges.json`). The vocabulary is closed, and each verb is defined by the
question it answers — if a proposed edge answers none of them, it belongs in
the page's prose instead.
| Verb | Question it answers | Since |
|---|---|---|
| `part_of` | Where does this live? What is it a piece of? | 1.5 |
| `uses` | What does this reach for at runtime? | 1.1 |
| `depends_on` | What breaks if I change this? | 1.1 |
| `produces` | Where does this output come from? | 1.5 |
| `configures` | What changes this thing's behaviour? | 1.5 |
| `validates` | What checks, tests, or judges this? | 1.5 |
| `implements` | What contract must this honour? | 1.5 |
| `caused` | Why did this happen? | 1.1 |
| `contradicts` | What disagrees with this, unresolved? | 1.1 |
| `supersedes` | What replaced this, and what did it replace? | 1.1 |
| `decided_by` | Who made this call? | 1.4 |
| `affects` | What does this decision constrain? | 1.4 |
| `has_expertise_in` | Who can answer questions on this? | 1.3 |
| `owns` | Who is responsible for this? | 1.3 |
| `mentioned_in` | Which source document discusses this? | 1.2 (lib indexes only) |
Record one direction per relationship — `part_of`, `supersedes`,
`depends_on` and `uses` are canonical, and the inverse is not stored as a
second edge. Record edges only from demonstrated evidence; an absent edge
beats a fabricated one, and a padded graph degrades retrieval rather than
improving it (in-degree is used as a ranking signal).
### Reserved scaffold
| Path | Since | Purpose |
|---|---|---|
| `wiki/index.md` | 1.1 | Routing table; the only page carrying `kb_schema_version` |
| `wiki/overview.md` | 1.1 | High-level map of the KB |
| `wiki/log.md` | 1.1 | Reverse-chronological change log for `wiki/` |
| `wiki/error-book.md` | 1.1 | Systemic issues with root cause and fix |
| `wiki/entities/` | 1.1 | Typed entity pages |
| `wiki/graph/` | 1.1 | `edges.json` plus the vocabulary in `index.md` |
| `wiki/query-gaps.md` | 1.2 | Questions the wiki could not answer, driving demand-driven ingest |
| `wiki/projects/` | 1.2 | Optional local query scopes |
| `wiki/decisions/` | 1.4 | Numbered, append-only decision records, with their own `log.md` |
Every subdirectory that groups pages carries its own `index.md`, so
navigation stays lazy.
---
## KB schema version history
### 1.5 — 2026-09-21 · evidence, fingerprints, and a completed edge vocabulary
Adopted from an analysis of [trailhq/Graft](https://github.com/trailhq/Graft),
a context layer for coding agents that keeps a derived code graph in sync via
content hashes rather than dates, and protects a user-authored block on every
regenerated node. Graft's store is disposable and regenerable; this one is not,
so most of its design does not transfer — but several mechanisms do, and two
of them closed real gaps here.
**Added:**
- **`## Crux`** — verbatim source excerpts alongside the synthesis. Lets
`ckb-retrieve` ground an answer without a round-trip to the archive (when
the fingerprint still matches), and makes drift detectable.
- **`## Notes`** — human-authored and protected everywhere. This closed a real
gap: `ckb-index-external` regenerates connector pages wholesale, so an
annotation written there was previously destroyed on the next refresh.
- **`## Sources`** — formalised as a reserved section with one fingerprinted
bullet per source. Previously an informal convention that `ckb-retrieve`
relied on but no skill actually specified.
- **`source_fingerprint` / `source_checked`** frontmatter.
- **Edge verbs** `part_of`, `produces`, `configures`, `validates`,
`implements`. `part_of` fixed a live inconsistency: `ckb-code-map` had been
writing it since template 1.7.0 while the schema never declared it.
**Changed:**
- `wiki/graph/index.md` rewritten as a question-per-verb table with
conventions on edge direction and evidence.
- `ckb-ingest` gained a blast-radius step: before writing, walk the graph
backwards from touched entities to find what the incoming material
confirms, extends or contradicts, and name the owners of affected pages.
Ingest had been additive-first, which is how a wiki accumulates two pages
that quietly disagree.
- `ckb-retrieve` fuses graph in-degree as one ranked list among several,
weighted below 1.0 — centrality is a prior, not evidence.
- Rule E (session start) now runs `lint_report.py --quick` alongside
`git status`: a deterministic one-line knowledge-rot signal costing no
model tokens.
- Lint gained checks 12 (fingerprint drift), 13 (crux verbatimness) and 14
(the protected-`## Notes` rule).
**Compatibility:** fully additive. A 1.4 page with none of the new sections or
fields is valid 1.5. A migration will never manufacture a `## Crux`
inventing quotes is precisely the failure that section exists to prevent.
### 1.4 — 2026-09-01 · decision records
**Added:** `type: decision` pages under `wiki/decisions/` as `NNNN-slug.md`,
with `status`, `decided_on`, `decided_by`, `affects`, `review_on`; the
`decided_by` and `affects` edge verbs; `wiki/decisions/index.md` and its own
`log.md`; the append-only rule. Answers "why is it like this", "who decided",
and "what changed that decision" as direct lookups instead of a full-text
guess. Owned by the `ckb-decide` skill.
### 1.3 — 2026-08-06 · people-to-topic edges
**Added:** the `has_expertise_in` and `owns` edge verbs, making "who knows
about X" and "who owns X" a graph lookup rather than a full-text search.
Recorded only from demonstrated evidence — attending a meeting is not
expertise, and a job title is not ownership.
### 1.2 — 2026-07-29 · query scopes, gaps, and connector-backed libs
**Added:** `wiki/projects/` (optional local query scopes grouping related
pages, sources and graph areas); `wiki/query-gaps.md` (failed searches
recorded as future ingest targets); `raw/archive/<YYYY-MM-DD>/` as the
agent-maintained filing destination; connector-backed `libs/<name>/` with a
user-authored `source.yaml`, an agent-owned generated index, and the
`mentioned_in` edge verb used inside those indexes.
### 1.1 — 2026-07-13 · initial schema
The first versioned contract, shipped with the initial commit. Established
the frontmatter field set (`type`, `resource`, `tldr`, `confidence`,
`quality`, `supersedes`/`superseded_by`, `last_updated`,
`freshness_window_days`, `retention`), the core edge verbs (`uses`,
`depends_on`, `caused`, `contradicts`, `supersedes`), the `wiki/` scaffold,
the cascade priority rule, and the recursive index-and-log convention.
There was never a 1.0: versioning began with the first published schema.
---
## Template version history
The tooling layer — `AGENTS.md`/`CLAUDE.md`, the skills, the scripts, the
docs. Independent of the content schema above.
| Version | Date | What landed | Schema |
|---|---|---|---|
| **1.8.0** | 2026-09-21 | Seven ideas adopted from Graft: crux/notes/fingerprints, `--quick` lint mode, in-degree ranking, ingest blast radius, completed edge vocabulary | → 1.5 |
| 1.7.0 | 2026-09-20 | Opt-in modules (`.agents/modules/`, `ckb-module`, `ckb.yaml`); the `software` module with `ckb-code-map` and `ckb-spec`; `ckb-reset`; OpenSpec docs | 1.4 |
| 1.6.1 | 2026-09-01 | Fixed false-positive broken-edge findings in the lint graph check | 1.4 |
| 1.6.0 | 2026-09-01 | `ckb-decide`; detection half of lint moved into `lint_report.py`; OKF export moved into `export_okf.py` | → 1.4 |
| 1.3.0 | 2026-08-06 | Rank fusion, dedupe and rerank in `ckb-retrieve`; expertise and ownership lookups | → 1.3 |
| 1.2.1 | 2026-07-29 | `AGENTS.md` compressed — workflows moved out into skills, leaving a small always-loaded rule set | 1.2 |
| 1.2.0 | 2026-07-29 | Project scopes, query gaps, source-verified retrieval | → 1.2 |
| 1.1.0 | 2026-07-20 | Connector-backed `libs/` with self-contained external source indexing (`ckb-index-external`) | 1.1 \* |
| 1.0.0 | 2026-07-17 | First tagged template: full skill set, `LICENSE`, `MANUAL`, bilingual docs | 1.1 |
\* Connector-backed libs shipped as tooling in 1.1.0, but the schema only
recorded them — the `source.yaml` contract, the generated index shape, the
`mentioned_in` verb — at 1.2, nine days later. The two numbers catching up
with each other like this is normal, and is why the schema column shows what
was in effect *after* each template release rather than what the release was
about.
Versions 1.4.0 and 1.5.0 were never published — the template jumped from
1.3.0 to 1.6.0 on 2026-09-01.
---
## Migrating between versions
Say **"upgrade the wiki"** or **"check for a newer template version"**. The
`ckb-upgrade` skill checks the canonical template repo, updates the tooling
layer in place, and — separately, and only after you explicitly confirm —
migrates existing `wiki/` content up to the current schema while preserving
every fact already collected.
The two halves are deliberately separate. Taking newer skills never rewrites
your pages, and a content migration is never silent: it reports what it
intends to change, batches anything it had to infer (such as a missing `type`)
for your confirmation, and logs every touched page in `wiki/log.md` marked as
a schema-migration backfill rather than new knowledge.
Because every schema version so far has been additive, an older wiki keeps
working unmigrated. Migration is worth doing to make the newer checks
meaningful — backfilled fingerprints are what give lint anything to verify —
not because anything is broken without it.
---
## Version & License
Current template version: [VERSION](VERSION). Current schema version: the
`kb_schema_version` field in `wiki/index.md`. Licensed under the
[Apache License 2.0](LICENSE).

341
CHANGELOG.pl.md Normal file
View file

@ -0,0 +1,341 @@
# Historia zmian i referencja schematu
*Przeczytaj to w: [English](CHANGELOG.md) | **Polski***
Ten plik śledzi dwie rzeczy: schemat strony dokładnie w takiej postaci, w
jakiej obowiązuje dziś, oraz to, jak projekt doszedł do swoich obecnych
numerów wersji.
Istnieją **dwa niezależne numery wersji** i nie są tym samym:
| Numer | Mieszka w | Opisuje | Kto go podnosi |
|---|---|---|---|
| `kb_schema_version` | frontmatter `wiki/index.md` | **kontrakt treści** — co strona może zawierać i co te pola znaczą | `ckb-upgrade` (przy potwierdzonej migracji), `ckb-module` (addytywnie, przy instalacji) |
| Wersja szablonu | [`VERSION`](VERSION) | **warstwa narzędziowa**`AGENTS.md`, skille, skrypty, dokumentacja | `ckb-upgrade`, gdy pobiera nowszy szablon |
Poruszają się niezależnie i jest to celowe. Możesz wziąć nowszy zestaw skilli
bez dotykania choćby jednej strony wiki, a wiki napisane pod starszy schemat
nadal działa — po to właśnie jest wersja schematu.
**Polityka wersjonowania.** Podnoś wersję **pomniejszą** przy zmianie
addytywnej: nowe pole opcjonalne, nowa opcjonalna sekcja treści, nowy
opcjonalny plik szkieletu. Podnoś wersję **główną** przy zmianie łamiącej
kompatybilność: zmiana lub usunięcie pola wymaganego albo zmiana istniejącej
zastrzeżonej konwencji nazw plików. Nigdy nie było podniesienia wersji
głównej — każda dotychczasowa wersja schematu była addytywna, więc dowolna
strona napisana od 2026-07-13 jest dziś nadal poprawna.
---
## Spis treści
1. [Aktualny schemat strony (1.5)](#aktualny-schemat-strony-kb_schema_version-15)
2. [Historia wersji schematu KB](#historia-wersji-schematu-kb)
3. [Historia wersji szablonu](#historia-wersji-szablonu)
4. [Migracja między wersjami](#migracja-między-wersjami)
---
## Aktualny schemat strony (`kb_schema_version: "1.5"`)
### Frontmatter — każda strona niezastrzeżona
Każdy plik `.md` w `wiki/` poza zastrzeżonymi (`index.md`, `log.md`) niesie
frontmatter YAML. Wymagane jest wyłącznie `type`; reszta jest opcjonalna i
preferowana tam, gdzie ma sens. Parser jest celowo prosty — wyłącznie płaskie
pary `klucz: wartość`, bez zagnieżdżeń.
| Pole | Wymagane | Znaczenie |
|---|---|---|
| `type` | **tak** | Otwarty ciąg: `person`, `project`, `concept`, `library`, `decision`, `playbook`, `repository`, `component`, … Nowe wartości są zawsze poprawne; czytelnicy tolerują nierozpoznane. |
| `resource` | nie | Kanoniczny URI autorytatywnego źródła zewnętrznego, które opisuje ta strona, trzymany oddzielnie od własnego komentarza wiki. |
| `tldr` | nie | Jednozdaniowe streszczenie zoptymalizowane pod odczyt przez LLM. To ono trafia do indeksu i decyduje, czy strona w ogóle zostanie otwarta. |
| `confidence` | nie | `0.0``1.0`. Potwierdzenie przez źródła. Ustawiane przy zapisie, zanika, jeśli nic go nie wzmacnia, rośnie przy nowym zgodnym źródle. |
| `quality` | nie | `0.0``1.0`. Samoocena struktury i cytowań samej strony. Poniżej `0.7` oflagowane do przeglądu. |
| `supersedes` | nie | Ścieżka do starszej strony, którą ta zastępuje. |
| `superseded_by` | nie | Ścieżka do nowszej strony, która zastąpiła tę. **Zawsze ustawiaj obie strony.** |
| `last_updated` | nie | `YYYY-MM-DD`. Kiedy zmieniła się *strona*. |
| `freshness_window_days` | nie | Liczba dni, po której lint oznacza stronę jako nieaktualną. Typowo: 90 dla strony wiki, 365 dla decyzji, 30 dla dokumentu z indeksu konektora. |
| `retention` | nie | `high` / `medium` / `low`. Strona `low` jest archiwizowana (nigdy usuwana) po 2× swoim oknie świeżości. |
| `source_fingerprint` | nie | *(1.5)* Skrót źródła, z którego zbudowano stronę — `sha256:<8 hex>` dla pliku lokalnego albo `etag:<wartość>` / `mtime:<iso8601>` dla elementu z konektora. |
| `source_checked` | nie | *(1.5)* `YYYY-MM-DD` — kiedy ten skrót był ostatnio zweryfikowany. To co innego niż `last_updated`: ponowne sprawdzenie, które nie wykryło zmiany, przesuwa to pole i zostawia `last_updated` w spokoju. |
Wyłącznie `wiki/index.md` niesie `kb_schema_version`. To deklaracja na
poziomie całego zbioru, nie pojedynczej strony — same strony nigdy jej nie
niosą.
### Frontmatter — strony decyzji
Strony z `type: decision` mieszkają w `wiki/decisions/` jako `NNNN-slug.md` i
dodają:
| Pole | Wymagane | Znaczenie |
|---|---|---|
| `status` | **tak** | `proposed` / `accepted` / `rejected` / `superseded` / `reversed`. Słownik zdefiniowany w `wiki/decisions/index.md` i walidowany przez lint. |
| `decided_on` | dla `accepted`/`rejected`/`reversed` | `YYYY-MM-DD`, data podjęcia decyzji. |
| `decided_by` | gdy wiadomo | Nazwiska po przecinku. Gdy naprawdę nie wiadomo, wpisz `unknown` zamiast pomijać pole — „nie wiemy, kto to zdecydował" samo w sobie warto zapisać. |
| `affects` | nie | Ścieżki wiki po przecinku, które ta decyzja ogranicza. |
| `review_on` | nie | `YYYY-MM-DD` do ponownego rozważenia. Lint zgłasza je po przekroczeniu daty. |
Rekordy decyzji są **tylko do dopisywania**. Decyzji nigdy nie przepisuje się
pod późniejszą zmianę zdania: zapisz nową, która ją zastępuje, a obie zostają
na wokandzie.
### Zastrzeżone sekcje treści
*(Nowość w 1.5.)* Cztery nagłówki `##` znaczą to samo na każdej stronie w
każdej warstwie kaskady. Wszystkie są opcjonalne; tam, gdzie występują,
znaczą dokładnie to i nic innego.
#### `## Sources`
Skąd wzięła się strona. Po jednym punkcie na źródło, każdy ze skrótem:
```markdown
## Sources
- `raw/archive/2026-09-21/kickoff-notes.md` — sha256:3f9a2c1e (checked 2026-09-21)
```
Okno świeżości to przypuszczenie, że źródło *mogło* się zmienić. Skrót to
fakt, czy *się zmieniło*. Lint przelicza skróty lokalne i oznacza to, co
faktycznie się zmieniło — a to inne i pilniejsze znalezisko niż strona, która
jedynie się zestarzała.
#### `## Crux`
Dosłowne fragmenty tych źródeł — dowód, nigdy parafraza — przypisane do
punktu źródła, z którego pochodzą:
```markdown
## 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`, sekcja „Access"
```
Roboczy zakres to od trzech do dziesięciu linijek. Crux zbliżający się
długością do streszczenia nad nim przestał być dowodem i stał się drugą kopią
źródła. Z cytowania zamiast parafrazowania wynikają dwie rzeczy: na pytanie
często da się odpowiedzieć ze strony zamiast z archiwum, a odpływ od źródła
staje się widoczny — streszczenie może po cichu oddalić się od źródła, cytat
albo wciąż się zgadza, albo nie.
Strona bez cytowalnego źródła po prostu nie ma `## Crux`. Pusty albo
sparafrazowany jest gorszy niż żaden, bo wygląda jak dowód.
#### `## Notes`
Pisane przez człowieka i **chronione**. Żaden skill nie może tej sekcji
nadpisać, przeformatować, streścić ani usunąć; regeneracja zachowuje ją co do
bajtu.
```markdown
## Notes
<!-- Twoje. Żaden skill tego nie nadpisuje. -->
```
Ma to największe znaczenie na stronach, które agent *regeneruje* — indeksy
konektorów, mapy kodu — gdzie cała reszta jest odrzucana i budowana od nowa
przy kolejnym przebiegu. To jedyne miejsce, w którym adnotacja przetrwa.
Strony decyzji celowo nie mają `## Notes`: nic ich nie regeneruje, a rekord
tylko-do-dopisywania ze swobodnie edytowalnym blokiem adnotacji zaprasza
dokładnie do tej wstecznej korekty, której zasada append-only ma zapobiegać.
### Słownik krawędzi
Relacje mieszkają w `wiki/graph/edges.json` (oraz we własnym
`graph/edges.json` każdego indeksu konektora). Słownik jest zamknięty, a każdy
czasownik zdefiniowany przez pytanie, na jakie odpowiada — jeśli proponowana
krawędź nie odpowiada na żadne z nich, jej miejsce jest w tekście strony.
| Czasownik | Pytanie, na jakie odpowiada | Od |
|---|---|---|
| `part_of` | Gdzie to mieszka? Czego jest częścią? | 1.5 |
| `uses` | Po co to sięga w czasie działania? | 1.1 |
| `depends_on` | Co się zepsuje, jeśli to zmienię? | 1.1 |
| `produces` | Skąd bierze się ten wynik? | 1.5 |
| `configures` | Co zmienia zachowanie tej rzeczy? | 1.5 |
| `validates` | Co to sprawdza, testuje albo ocenia? | 1.5 |
| `implements` | Jakiego kontraktu to musi dotrzymać? | 1.5 |
| `caused` | Dlaczego to się stało? | 1.1 |
| `contradicts` | Co jest z tym sprzeczne i nierozstrzygnięte? | 1.1 |
| `supersedes` | Co to zastąpiło i co ono zastąpiło? | 1.1 |
| `decided_by` | Kto podjął tę decyzję? | 1.4 |
| `affects` | Co ta decyzja ogranicza? | 1.4 |
| `has_expertise_in` | Kto potrafi odpowiedzieć na pytania o to? | 1.3 |
| `owns` | Kto za to odpowiada? | 1.3 |
| `mentioned_in` | Który dokument źródłowy o tym mówi? | 1.2 (tylko indeksy libs) |
Zapisuj po jednym kierunku na relację — `part_of`, `supersedes`,
`depends_on` i `uses` są kanoniczne, a odwrotność nie jest przechowywana jako
druga krawędź. Zapisuj krawędzie wyłącznie na podstawie wykazanych dowodów;
brak krawędzi jest lepszy niż krawędź zmyślona, a napompowany graf pogarsza
wyszukiwanie zamiast je poprawiać (stopień wejściowy jest sygnałem
rankingowym).
### Zastrzeżony szkielet
| Ścieżka | Od | Do czego służy |
|---|---|---|
| `wiki/index.md` | 1.1 | Tabela routingu; jedyna strona niosąca `kb_schema_version` |
| `wiki/overview.md` | 1.1 | Mapa KB z lotu ptaka |
| `wiki/log.md` | 1.1 | Dziennik zmian `wiki/` w odwrotnej chronologii |
| `wiki/error-book.md` | 1.1 | Problemy systemowe wraz z przyczyną i naprawą |
| `wiki/entities/` | 1.1 | Typowane strony encji |
| `wiki/graph/` | 1.1 | `edges.json` plus słownik w `index.md` |
| `wiki/query-gaps.md` | 1.2 | Pytania, na które wiki nie umiało odpowiedzieć, napędzające ingest sterowany popytem |
| `wiki/projects/` | 1.2 | Opcjonalne lokalne zakresy zapytań |
| `wiki/decisions/` | 1.4 | Numerowane rekordy decyzji tylko-do-dopisywania, z własnym `log.md` |
Każdy podkatalog grupujący strony niesie własny `index.md`, dzięki czemu
nawigacja pozostaje leniwa.
---
## Historia wersji schematu KB
### 1.5 — 2026-09-21 · dowody, skróty źródeł i dokończony słownik krawędzi
Zaadaptowane z analizy [trailhq/Graft](https://github.com/trailhq/Graft),
warstwy kontekstu dla agentów kodujących, która utrzymuje wyprowadzony graf
kodu w zgodzie ze źródłem przez skróty treści, a nie daty, i chroni blok
pisany przez użytkownika na każdym regenerowanym węźle. Magazyn Graftu jest
jednorazowy i odtwarzalny; ten nie jest, więc większość jego projektu się nie
przenosi — ale kilka mechanizmów tak, a dwa z nich zamknęły tu realne luki.
**Dodano:**
- **`## Crux`** — dosłowne fragmenty źródeł obok syntezy. Pozwala
`ckb-retrieve` ugruntować odpowiedź bez powrotu do archiwum (gdy skrót się
wciąż zgadza) i czyni odpływ od źródła wykrywalnym.
- **`## Notes`** — pisane przez człowieka i chronione wszędzie. To zamknęło
realną lukę: `ckb-index-external` regeneruje strony konektora w całości,
więc napisana tam adnotacja ginęła dotąd przy kolejnym odświeżeniu.
- **`## Sources`** — sformalizowane jako sekcja zastrzeżona z jednym
punktem ze skrótem na źródło. Wcześniej nieformalna konwencja, na której
`ckb-retrieve` polegało, ale której żaden skill faktycznie nie określał.
- **`source_fingerprint` / `source_checked`** we frontmatterze.
- **Czasowniki krawędzi** `part_of`, `produces`, `configures`, `validates`,
`implements`. `part_of` naprawiło żywą niespójność: `ckb-code-map`
zapisywał go od szablonu 1.7.0, a schemat nigdy go nie deklarował.
**Zmieniono:**
- `wiki/graph/index.md` przepisany jako tabela pytanie-na-czasownik wraz z
konwencjami dotyczącymi kierunku krawędzi i dowodów.
- `ckb-ingest` dostał krok promienia rażenia: przed zapisem przejdź graf
wstecz od dotkniętych encji, żeby ustalić, co nadchodzący materiał
potwierdza, rozszerza albo z czym jest sprzeczny, i wskaż właścicieli
dotkniętych stron. Ingest był dotąd przede wszystkim addytywny, a tak
właśnie wiki gromadzi dwie strony, które po cichu się ze sobą nie zgadzają.
- `ckb-retrieve` wtapia stopień wejściowy grafu jako jedną z rankowanych list,
z wagą poniżej 1.0 — centralność jest przesłanką, nie dowodem.
- Reguła E (start sesji) uruchamia teraz `lint_report.py --quick` obok
`git status`: deterministyczny jednoliniowy sygnał gnicia wiedzy, niekosztujący
żadnych tokenów modelu.
- Lint zyskał kontrole 12 (odpływ skrótu źródła), 13 (dosłowność sekcji Crux)
i 14 (zasada chronionego `## Notes`).
**Kompatybilność:** w pełni addytywna. Strona 1.4 bez żadnej z nowych sekcji
i pól jest poprawną stroną 1.5. Migracja nigdy nie wytworzy `## Crux`
zmyślanie cytatów to dokładnie ta porażka, której ta sekcja ma zapobiegać.
### 1.4 — 2026-09-01 · rekordy decyzji
**Dodano:** strony `type: decision` w `wiki/decisions/` jako `NNNN-slug.md`,
z `status`, `decided_on`, `decided_by`, `affects`, `review_on`; czasowniki
krawędzi `decided_by` i `affects`; `wiki/decisions/index.md` wraz z własnym
`log.md`; zasadę append-only. Odpowiada na „dlaczego jest tak, jak jest",
„kto zdecydował" i „co zmieniło tę decyzję" bezpośrednim wyszukaniem zamiast
zgadywania pełnotekstowego. Właścicielem jest skill `ckb-decide`.
### 1.3 — 2026-08-06 · krawędzie osobatemat
**Dodano:** czasowniki krawędzi `has_expertise_in` i `owns`, dzięki którym
„kto wie o X" i „kto jest właścicielem X" to wyszukanie w grafie, a nie
przeszukiwanie pełnotekstowe. Zapisywane wyłącznie na podstawie wykazanych
dowodów — obecność na spotkaniu to nie ekspertyza, a stanowisko to nie
własność.
### 1.2 — 2026-07-29 · zakresy zapytań, luki i libs oparte na konektorach
**Dodano:** `wiki/projects/` (opcjonalne lokalne zakresy zapytań grupujące
powiązane strony, źródła i obszary grafu); `wiki/query-gaps.md` (nieudane
wyszukiwania zapisane jako przyszłe cele ingestu);
`raw/archive/<YYYY-MM-DD>/` jako utrzymywane przez agenta miejsce
archiwizacji; `libs/<name>/` oparte na konektorach, z pisanym przez
użytkownika `source.yaml`, należącym do agenta generowanym indeksem oraz
czasownikiem krawędzi `mentioned_in` używanym wewnątrz tych indeksów.
### 1.1 — 2026-07-13 · pierwszy schemat
Pierwszy wersjonowany kontrakt, wydany wraz z pierwszym commitem. Ustanowił
zestaw pól frontmatteru (`type`, `resource`, `tldr`, `confidence`, `quality`,
`supersedes`/`superseded_by`, `last_updated`, `freshness_window_days`,
`retention`), podstawowe czasowniki krawędzi (`uses`, `depends_on`, `caused`,
`contradicts`, `supersedes`), szkielet `wiki/`, zasadę priorytetu kaskady i
rekurencyjną konwencję indeksu i dziennika.
Wersji 1.0 nigdy nie było: wersjonowanie zaczęło się od pierwszego
opublikowanego schematu.
---
## Historia wersji szablonu
Warstwa narzędziowa — `AGENTS.md`/`CLAUDE.md`, skille, skrypty, dokumentacja.
Niezależna od schematu treści powyżej.
| Wersja | Data | Co weszło | Schemat |
|---|---|---|---|
| **1.8.0** | 2026-09-21 | Siedem pomysłów zaadaptowanych z Graftu: crux/notes/skróty źródeł, tryb `--quick` lintu, ranking po stopniu wejściowym, promień rażenia w ingeście, dokończony słownik krawędzi | → 1.5 |
| 1.7.0 | 2026-09-20 | Opcjonalne moduły (`.agents/modules/`, `ckb-module`, `ckb.yaml`); moduł `software` z `ckb-code-map` i `ckb-spec`; `ckb-reset`; dokumentacja OpenSpec | 1.4 |
| 1.6.1 | 2026-09-01 | Naprawa fałszywie dodatnich znalezisk uszkodzonych krawędzi w kontroli grafu | 1.4 |
| 1.6.0 | 2026-09-01 | `ckb-decide`; wykrywająca połowa lintu przeniesiona do `lint_report.py`; eksport OKF przeniesiony do `export_okf.py` | → 1.4 |
| 1.3.0 | 2026-08-06 | Fuzja rankingów, deduplikacja i ponowny ranking w `ckb-retrieve`; wyszukiwanie ekspertyzy i własności | → 1.3 |
| 1.2.1 | 2026-07-29 | `AGENTS.md` skompresowany — przepływy przeniesione do skilli, zostawiając mały, zawsze ładowany zestaw reguł | 1.2 |
| 1.2.0 | 2026-07-29 | Zakresy projektów, luki zapytań, wyszukiwanie weryfikowane względem źródła | → 1.2 |
| 1.1.0 | 2026-07-20 | `libs/` oparte na konektorach z samodzielnym indeksowaniem źródeł zewnętrznych (`ckb-index-external`) | 1.1 \* |
| 1.0.0 | 2026-07-17 | Pierwszy otagowany szablon: pełny zestaw skilli, `LICENSE`, `MANUAL`, dokumentacja dwujęzyczna | 1.1 |
\* `libs/` oparte na konektorach weszły jako narzędzia w 1.1.0, ale schemat
zapisał je — kontrakt `source.yaml`, kształt generowanego indeksu, czasownik
`mentioned_in` — dopiero w 1.2, dziewięć dni później. Takie doganianie się
tych dwóch numerów jest normalne i dlatego kolumna schematu pokazuje, co
obowiązywało *po* danym wydaniu szablonu, a nie czego to wydanie dotyczyło.
Wersje 1.4.0 i 1.5.0 nigdy nie zostały opublikowane — szablon przeskoczył z
1.3.0 na 1.6.0 dnia 2026-09-01.
---
## Migracja między wersjami
Powiedz **„upgrade the wiki"** albo **„check for a newer template version"**.
Skill `ckb-upgrade` sprawdza kanoniczne repozytorium szablonu, aktualizuje
warstwę narzędziową w miejscu i — osobno, i wyłącznie po twoim wyraźnym
potwierdzeniu — migruje istniejącą treść `wiki/` do bieżącego schematu,
zachowując każdy już zebrany fakt.
Te dwie połowy są celowo rozdzielone. Wzięcie nowszych skilli nigdy nie
przepisuje twoich stron, a migracja treści nigdy nie jest cicha: zgłasza, co
zamierza zmienić, zbiera wszystko, co musiała wywnioskować (na przykład
brakujące `type`), do twojego potwierdzenia i loguje każdą dotkniętą stronę w
`wiki/log.md` z adnotacją, że to uzupełnienie migracyjne schematu, a nie nowa
wiedza.
Ponieważ każda dotychczasowa wersja schematu była addytywna, starsze wiki
działa bez migracji. Migrację warto zrobić po to, żeby nowsze kontrole miały
sens — uzupełnione skróty źródeł dają lintowi cokolwiek do weryfikacji — a
nie dlatego, że bez niej coś jest zepsute.
---
## Wersja i licencja
Aktualna wersja szablonu: [VERSION](VERSION). Aktualna wersja schematu: pole
`kb_schema_version` w `wiki/index.md`. Licencja:
[Apache License 2.0](LICENSE).

View file

@ -6,8 +6,9 @@ This is the human-facing manual for working with a Cascade Knowledge Base
(this repo). It's written for the *person* using the wiki, not the agent —
for the agent's own operating rules, see [AGENTS.md](AGENTS.md) /
[CLAUDE.md](CLAUDE.md). For a feature-by-feature technical overview, see
[README.md](README.md). This document is task-oriented: "I want to do X —
what do I say, and what happens?"
[README.md](README.md). For the page schema in full and the version
histories behind it, see [CHANGELOG.md](CHANGELOG.md). This document is
task-oriented: "I want to do X — what do I say, and what happens?"
Everywhere below, "say" means typing it to whatever AI coding agent you're
using against this repo (Claude Code, or another agent that reads
@ -273,8 +274,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 +714,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
<!-- Yours. Never rewritten by any skill. -->
```
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 +738,7 @@ graph stay in sync with what you changed.
| `libs/<name>/` (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/<name>/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/<name>/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/<name>/{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/<name>/{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. |

View file

@ -6,7 +6,9 @@ To jest podręcznik dla *człowieka* korzystającego z Cascade Knowledge Base
(tego repozytorium) — nie dla agenta. Zasady działania samego agenta
znajdziesz w [AGENTS.md](AGENTS.md) / [CLAUDE.md](CLAUDE.md). Techniczny,
funkcja-po-funkcji przegląd znajdziesz w [README.md](README.md) (lub
[README.pl.md](README.pl.md)). Ten dokument jest zorientowany na zadania:
[README.pl.md](README.pl.md)). Pełny schemat strony wraz z historią obu
numerów wersji znajdziesz w [CHANGELOG.pl.md](CHANGELOG.pl.md). Ten dokument
jest zorientowany na zadania:
„chcę zrobić X — co mam powiedzieć i co się wtedy stanie?”
Wszędzie poniżej „powiedz” oznacza napisanie tego do dowolnego agenta AI,
@ -285,8 +287,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 +754,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
<!-- Twoje. Żaden skill tego nie nadpisuje. -->
```
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 +778,7 @@ zmieniłeś.
| `libs/<name>/` (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/<name>/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/<name>/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/<name>/{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/<name>/{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. |

View file

@ -18,6 +18,9 @@ If this KB documents software you're building, the optional `software` module
adds `src/` repositories and spec-driven development — see
[OPENSPEC.md](OPENSPEC.md) ([Polski](OPENSPEC.pl.md)).
For the page schema in full, and for how both version numbers got where they
are, see [CHANGELOG.md](CHANGELOG.md) ([Polski](CHANGELOG.pl.md)).
---
## Directory Structure
@ -216,6 +219,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 +231,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 +352,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

View file

@ -19,6 +19,9 @@ Jeśli ta baza dokumentuje tworzone przez Ciebie oprogramowanie, opcjonalny modu
`software` dodaje repozytoria w `src/` i pracę sterowaną specyfikacją — zobacz
[OPENSPEC.pl.md](OPENSPEC.pl.md) ([English](OPENSPEC.md)).
Pełny schemat strony oraz historię obu numerów wersji znajdziesz w
[CHANGELOG.pl.md](CHANGELOG.pl.md) ([English](CHANGELOG.md)).
---
## Struktura katalogów
@ -227,6 +230,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 +249,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 +277,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 +381,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

View file

@ -1 +1 @@
1.7.0
1.8.0

View file

@ -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.)*

View file

@ -1,5 +1,5 @@
---
kb_schema_version: "1.4"
kb_schema_version: "1.5"
---
# Knowledge Base Index

View file

@ -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`.
---

View file

@ -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

View file

@ -0,0 +1,81 @@
# 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:** <https://github.com/trailhq/Graft> (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.
## [2026-09-21] Docs — CHANGELOG.md / CHANGELOG.pl.md
Added a bilingual changelog + schema reference at the repo root, following the
existing `.md`/`.pl.md` doc convention. Contents: the full current page schema
(frontmatter for all pages and for decisions, the three reserved body sections,
the closed edge vocabulary with a "since" column, the reserved scaffold), the
KB schema version history 1.1-1.5, the template VERSION history 1.0.0-1.8.0,
and a migration section.
History reconstructed from git, not from memory: `VERSION` and
`wiki/index.md` were read back at each commit that changed them, and each
release's diff was inspected to describe what it actually contained.
Two facts worth keeping:
- There was never a schema 1.0. Versioning began at 1.1 (c56348b, 2026-07-13).
- Template 1.4.0 and 1.5.0 were never published — VERSION jumped 1.3.0 -> 1.6.0
on 2026-09-01. Recorded as a note rather than papered over.
- Connector-backed libs shipped in template 1.1.0 but the schema only recorded
them at 1.2, nine days later; the table's schema column shows what was in
effect after each release, with a footnote explaining the lag.
Registered the new files with `ckb-init` (copied verbatim into a new KB) and
`ckb-upgrade` (taken wholesale from upstream, never merged — upstream is
authoritative about its own history). Cross-linked from README.md,
README.pl.md, MANUAL.md, MANUAL.pl.md.