Files
chemenu/kb/CONTRACT.md
T
torbenandClaude Opus 5.5 283cdae8be
CI / verify (push) Successful in 5m40s
CI / pwsh (push) Successful in 1m59s
Release / release (push) Successful in 35s
feat: export guidelines - the guideline pages as a generated GUIDELINES.md, pushed into the captured repositories behind the Guideline Push Gate (#179)
Files changed:
- AGENTS.md
- CHANGES.md
- README.md
- VERSION
- docs/why-gates-are-code.md
- instructions/gates.md
- instructions/kb-profiles.md
- kb/CONTRACT.md
- kb/CONVENTIONS.md
- kb/CONVENTIONS.md.template
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/export_cmd.py
- tools/chemenu/commands/page_ops.py
- tools/chemenu/commands/raw_cmd.py
- tools/chemenu/commands/search.py
- tools/chemenu/guideline_export.py
- tools/chemenu/kb_scan.py
- tools/chemenu/repo_capture.py
- tools/chemenu/search/filters.py
- tools/chemenu/tests/test_cli.py
- tools/chemenu/tests/test_export_guidelines.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
2026-10-06 06:48:07 +02:00

363 lines
22 KiB
Markdown

# kb/ - Knowledge Layer Contract
The compiled knowledge layer, and the third stage of the pipeline
`raw/` -> `kb/` -> `reports/`. Everything here is written and maintained by the LLM from
material in `raw/`, and is expected to stay correct without being re-derived.
**Quality goal:** a page should answer a future question *without* re-reading the source it
came from. If answering still requires the raw file, the page is incomplete.
This file holds the rules that apply in **every** collection **and in every instance**. That
second half is the cut: what is written here is enforced by `tools/wikitool` or follows from
how it works, so it is identical everywhere and `dist export` ships it verbatim.
**What an instance decides for itself is next door, in
[kb/CONVENTIONS.md](CONVENTIONS.md)** - the language pages are written in, the headings its two
generated regions render under, the naming forms, the tone, the hedging rule, which pages leave the wiki as guidelines. That file binds exactly as this one does; it is simply owned by the instance
rather than by the stack, so the distribution ships only its `.template` and the instance writes
the real one. Read both, plus the target collection's `kb/<name>/COLLECTION.md` (also
instance-owned), before writing or editing a page.
The split is by **who may change the sentence**, not by what it is about. Language, tone and
naming used to sit here, which meant every instance that answered "not German" to
`setup-instance.md` was locally editing a file the stack also ships - and a merge from upstream
would quietly hand it back.
Structural facts (which frontmatter fields exist, which are required, what the body skeleton
looks like) are in neither - they belong to the type-specs and are printed by
`tools/wikitool types describe <type>`. Never hand-write frontmatter; scaffold with
`tools/wikitool new <type> --name "<Name>" --set field=value ...`.
<!-- wikitool:toc -->
## Contents
- [Collections](#collections)
- [Generated files](#generated-files)
- [Titles are identifiers](#titles-are-identifiers)
- [Every page should](#every-page-should)
- [Quotation cap](#quotation-cap)
- [Language and identifiers](#language-and-identifiers)
- [Generated regions](#generated-regions)
- [Linking](#linking)
- [Provenance and citation](#provenance-and-citation)
- [Pages that leave the wiki: guidelines](#pages-that-leave-the-wiki-guidelines)
- [What does not belong here](#what-does-not-belong-here)
<!-- /wikitool:toc -->
## Collections
`kb/` is a **namespace, not a collection**. It carries no `COLLECTION.md` of its own.
A directory under `kb/` is a **collection** exactly when it contains a `COLLECTION.md`. That
file is the local authoring contract for every page in the subtree, and it belongs to the
instance: it declares in its frontmatter which profile from
[instructions/kb-profiles.md](../instructions/kb-profiles.md) it adopted, and whether the stack
resolves against it by name.
| Field | Means |
|---|---|
| `profile:` | Which catalogue entry this contract started from, or `none`. Free text - the catalogue is a palette, not an enum, and a collection an instance invented has no entry to name |
| `required_by_stack:` | Whether `wikitool` itself depends on this collection *by name*. Not the instance's to choose: `docs verify` checks it against the stack's own list. `kb/sources/` is `true` - `sources coverage`, `[^cite-id]` resolution and `kb/provenance.md` all resolve against that name - and everything else is `false` |
- A subdirectory *inside* a collection is an **area**. It inherits the enclosing contract and
must not carry a `COLLECTION.md` of its own - `kb/entities/systems/` is an area of
`kb/entities/`.
- **An area is as deep as a page goes.** `kb/<collection>/<page>.md` and
`kb/<collection>/<area>/<page>.md` are the two depths a page may sit at; nothing goes a level
deeper. A further subdirectory is not a second-level area - it is invisible to the generated
catalog, which reads exactly two path segments below `kb/` and folds anything past them into
the area's own table silently, with no location of its own. That is why
`wikitool lint`'s `nested_pages` finding is a hard error rather than an advisory one like
`misplaced_pages`: a misplaced page still catalogs correctly from the wrong place, a nested
one makes the catalog itself wrong. A grouping axis that does not come from a type-spec's
`layout:` - project owner was the case that surfaced this - does not earn a second directory
level; it goes into frontmatter instead.
- A `COLLECTION.md` nested inside another collection is invalid.
- `COLLECTION.md` appears **nowhere outside `kb/`**. `raw/`, `types/`, `tools/`, `reports/`
and `instructions/` are not collections and carry a `CONTRACT.md` or a root type-spec
instead.
`tools/wikitool docs verify` enforces all three.
| Collection | Holds | Contract |
|------------|-------|----------|
| `kb/entities/` | Concrete things: codebases, deployed systems, tools, technologies, people | [entities/COLLECTION.md](entities/COLLECTION.md) |
| `kb/concepts/` | Architectures, patterns, protocols, workflows, decisions, recurring problems | [concepts/COLLECTION.md](concepts/COLLECTION.md) |
| `kb/sources/` | One summary page per ingested source, carrying its `raw_files:` provenance | [sources/COLLECTION.md](sources/COLLECTION.md) |
| `kb/comparisons/` | Structured comparisons of two or more existing pages | [comparisons/COLLECTION.md](comparisons/COLLECTION.md) |
| `kb/gtd/` | One page per committed initiative (a GTD project): goal, participants, durable status, open loops | [gtd/COLLECTION.md](gtd/COLLECTION.md) |
The five rows above are this instance's collections, not a fixed set. **Adding one:**
`mkdir kb/<name>` and write a `kb/<name>/COLLECTION.md` with the two fields above. Collections
are discovered by contract presence, so no code change is needed. A collection only becomes
*writable* once some type-spec declares a matching `base_dir:`. Renaming or dropping one is the
instance's call too - except where `required_by_stack: true` says otherwise.
**Where a page goes** is decided by its type-spec, never by hand - see
[types/type-spec.md](../types/type-spec.md).
## Generated files
Never hand-edit these; they are produced by `tools/wikitool`:
| File | Produced by |
|------|-------------|
| `kb/index.md` | `wikitool index rebuild` - the catalog **map**: statistics, counts, links |
| `kb/<collection>/INDEX.md` and `kb/<collection>/<area>/INDEX.md` | `wikitool index rebuild` - the page tables |
| `kb/log.md` | `wikitool log append` |
| `kb/provenance.md` | `wikitool sources rebuild-index` |
To *find* a page, search rather than read the catalog: `tools/wikitool search "<text>"`, or
`tools/wikitool search --field <predicate>` for a structured query over frontmatter.
## Titles are identifiers
**The filename stem *is* the page title, and `[[wikilinks]]` must match it exactly.** That is
not a naming preference; it is the wiki's only way to address a page. `wikitool lint` reports an
H1 that stops matching its title, `rename`/`rm` rewrite every reference to a stem, and a
`[^cite-id]` resolves through one.
**A wikilink is one token and is never wrapped across lines.** When prose is broken at a fixed
column, the break goes before or after `[[...]]`, never inside it: a renderer does not reliably
read a link with a line break in it as a link. `wikitool lint` reports one as a *Wrapped
Wikilinks* hard error, naming the title it folds to - the graph, `rename` and `rm` already read
it as that title, so the fix is only to put it back on one line.
**A wikilink may name a section of its page: `[[Title#Section]]`.** The title part is the
reference - the graph counts it as a link to `Title`, and `rename` carries the anchor along - so
an anchor never makes a link broken. It can make one stale: a section renamed, or promoted to a
page of its own ([instructions/page-lifecycle.md](../instructions/page-lifecycle.md)), leaves the
link reaching the right page and the wrong place. `wikitool lint` reports an anchor that names no
heading on its page as *Broken Anchors*, advisory - compared at any heading level, without case,
inline-code backticks or extra whitespace. A section is never a target in `related:`; an edge
points at a page.
Which *form* those titles take - spaces or kebab-case, singular or plural, what prefixes a
decision record - is the instance's, in
[kb/CONVENTIONS.md § Naming](CONVENTIONS.md#naming).
**A title is also a file name, so it must be one on every platform** - Windows and macOS as
well as Linux, checked wherever the command runs: a corpus written on Linux is checked out on
the others, and a title that Linux accepts and Windows refuses breaks every clone there. The full
title counts, after any `title_prefix`. A title must not:
- be empty, or end with a dot or a space
- contain `<` `>` `:` `"` `/` `\` `|` `?` `*` or a control character
- start, before its first dot and regardless of case, with a Windows device name (`CON`, `PRN`,
`AUX`, `NUL`, `COM0`-`COM9`, `LPT0`-`LPT9`, and the superscript forms `COM¹`-`COM³`,
`LPT¹`-`LPT³`) or with a name the stack itself keeps beside a page (`INDEX`, `COLLECTION`)
- collide with another page once both are normalized to NFC and compared by `casefold` - NTFS and
APFS fold case, and APFS folds NFC and NFD as well
`wikitool new` and `wikitool rename` refuse such a title (`new` for every type, whatever root it
writes to; `rename` only for `--to`, so a page that already breaks the rule can always be renamed
away from it), and never write over an existing file. `wikitool lint` reports existing pages that
break the rule as Unportable Titles, a hard error at every `kb_version`.
**A path has a budget too.** Windows counts 259 characters for a whole path, the folder the
instance is checked out into included, and long paths are off on the target system. The path of
any file below the instance root - `kb/` page or `raw/` source - therefore stays at **160
characters or fewer**, written with `/` and counted in UTF-16 code units, which is how Windows
counts: an emoji outside the Basic Multilingual Plane takes two. The folder limit that `doctor`
checks is the other half of the same sum.
`wikitool new` (every root), `wikitool rename` (`--to` only, also under `--dry-run`), `wikitool
move` (a single page, and `--reconcile`, which skips and names such a target) and `wikitool raw
accept` (the target under `raw/`; the remedy is renaming the file in `incoming/` - for a folder,
a shorter folder name or shorter names inside it) refuse a path
over the budget before writing anything. `wikitool lint` reports existing files over it as Long
Paths - advisory, not a hard error, so a corpus that predates the budget still passes
`--fail-on-error`; the fix is `wikitool rename`.
## Every page should
- [ ] Carry a clear, descriptive title and a summary near the top
- [ ] Use consistent terminology with the rest of the wiki
- [ ] Link to the entities and concepts it mentions, and declare an edge where the relationship
is worth naming - in the direction this page asserts it, not in both
- [ ] Cite its hard facts (see [Provenance and citation](#provenance-and-citation))
- [ ] Duplicate no existing page
- [ ] Leave no section of its scaffold unwritten - `wikitool lint` reports a `##` section that
still holds nothing but its template's `TODO` placeholders as *Unfilled Template Sections*
(advisory). Write it from a source, or retire the page
([instructions/page-lifecycle.md](../instructions/page-lifecycle.md)); a field the source
does not give may keep its `TODO` beside written lines
- [ ] Appear in the catalog (guaranteed by `wikitool index rebuild`)
## Quotation cap
At most 2 blockquotes per page - a blockquote being a run of consecutive `>` lines, code masked
out first, so a `>` inside a fenced shell transcript is a prompt rather than a quotation.
`wikitool lint` reports overages as advisory, since exceeding the cap can be a legitimate
judgment call - but the page should carry the knowledge itself, not delegate it to quotations.
The cap is about how much of the page you let quotes carry, not how long a wrapped quotation
runs; it does not apply to text you are citing verbatim from a source.
The register those lines are written in - what counts as a buzzword, what filler is refused -
is the instance's, in [kb/CONVENTIONS.md § Tone](CONVENTIONS.md#tone).
## Language and identifiers
*Which* language pages are written in is [kb/CONVENTIONS.md](CONVENTIONS.md)'s to say. What
follows here is the part that is not a choice, because the tool resolves against it.
Every line of a page is either **prose** or an **identifier**. Only prose is translated.
**Prose:** descriptions and definitions, `## Key Information` values, `## Details` body text, a
source page's Summary / Key Takeaways / Action Items / Not Extracted, and `summary:`.
**Identifiers - never translated, in any language:**
| Identifier | Why |
|---|---|
| Page titles, and the H1 that repeats one | A title is the wiki's only identifier for a page and follows the subject's own established name - see [Titles are identifiers](#titles-are-identifiers). `wikitool lint` reports an H1 that stops matching its title |
| The subtype value on the generated `**Typ:**` line | It renders a schema enum value (`technology`, `workflow`), which `search --field` filters on. The label is prose; the value is not |
| `tags:` | Search keys, not prose |
| Commands, paths, config keys, hostnames, code | They are what they are |
| Quotations | Quoted verbatim in the source's own language |
Which foreign technical terms stay untranslated inside that prose is a judgment call the
instance records - see [kb/CONVENTIONS.md § Language](CONVENTIONS.md#language).
**A source in another language** is still summarized in the KB language: a source page is
evidence *about* a source, not a substitute for it. Quote verbatim in the original language and
record the raw file's language in `source_language:`.
### Generated regions
Two regions of a page body are **generated**, not authored: the links region `xref` owns and the
footnotes region `cite` owns. Each sits between a marker pair:
```markdown
<!-- wikitool:links -->
## Beziehungen
- **depends-on:** [[Hermes]]
<!-- /wikitool:links -->
```
The marker is what the tool locates the region by, and everything between the markers -
**heading included** - is replaced wholesale on the next write. An author never edits inside
them; anything left there is overwritten without warning, exactly as in `kb/index.md`. A region
with nothing to show is absent rather than empty.
The heading is therefore a *rendering* value, taken from `kb/CONVENTIONS.md`'s `sections:`. No
heading text exists in the compiler, and nothing matches on it: changing the declaration
re-renders the words on the next write and cannot split a page.
That is not how it used to work. The tool located these regions by matching their heading text,
which made a translated heading a structural fact - and made the region's *end* a guess. It ran
to the next heading, and before that to the end of the file, which silently deleted whatever sat
after it on eight pages. Any *other* heading a page carries is ordinary prose.
## Linking
**An edge is authored in one direction**, on the page that asserts it, and carries a label that
is a machine value rather than prose:
```yaml
related:
- depends-on: Hermes
```
Created with `tools/wikitool xref add --a "<A>" --b "<B>" --rel <label>`, never by hand-editing
`related:` or the rendered bullet. Say the sentence before choosing the label - `[A] <label>
[B]` - and if it only reads true backwards, the edge belongs on the other page.
**A reverse edge is a separate decision, not a mirror.** Write one when it independently helps a
reader at the other end; do not write one to make the graph symmetric. Navigation does not
depend on it either way: `index rebuild` renders the inbound view from the graph, completely and
without maintenance.
Which labels exist is [instructions/link-taxonomy.md](../instructions/link-taxonomy.md), a
palette that binds nothing. Which of them a page may *use* is its own collection's `outbound:`
block, per destination - the **source** collection decides, because the rules that govern an
edge are the rules of the collection asserting it. `xref add` refuses an unauthorised label and
`lint` reports one.
A page is expected to have at least one inbound edge; `wikitool lint` reports orphans.
Comparison pages are exempt - they are reached through the catalog. Directional edges mean more
pages qualify than under the old mirrored model, and that is the check measuring reachability
rather than measuring whether `xref` ran.
Renaming a page, deleting one, or dropping a single reference are tool operations with their
own procedure: see [instructions/page-lifecycle.md](../instructions/page-lifecycle.md).
## Provenance and citation
Every claim is either traceable to a raw file or explicitly marked as not.
- **`provenance:`** on every entity/concept page - `sourced` (every substantive claim traces
to a raw file), `general` (LLM general knowledge, no raw backing), or `mixed` (both; put the
unsourced part under a `## General Guidance (unsourced)` heading).
- **`raw_files:`** on every source page - concrete existing file paths under `raw/`, never a
directory and never a bare URL. For an external article also set `source_url:`, but
`raw_files:` must still point at the local copy under `raw/` - for a page captured with
`raw fetch`, both the received `.html` and the derived `.md`.
- **One source page may cover many raw files.** A folder of related documents becomes a single
page listing all of them, not one page per file.
- **A `[^cite-id]` footnote** appended to any *specific hard fact*: an IP, port, version, path,
command, or config value. `tools/wikitool cite add --page "<Title>" --source "Source - X"
[--file <qualifier>]` mints the id, upserts its `[[Source - X]]` (or
`[[Source - X|storage-model.md]]` for a multi-file source) definition in the page's trailing
Footnotes block (named per [Section headings](#section-headings)), and adds `Source - X` to
`sources:` - unless the page *is* `Source - X`, citing one of its own raw files, since a page
never lists its own title there. It prints the marker to paste at
the fact; placing it is still manual. Never hand-type a cite-id (AGENTS.md invariant 1). This
differs from a plain `[[Source - X]]` link, which only means "related to".
- **Notation inside code is notation, not a reference.** A `[^cite-id]` or a `[[wikilink]]`
written in backticks or a fenced block is read as an example: the citation does not count and
the link does not exist. That is what lets a page document this stack's own syntax. It also
means a marker appended to a line *inside* a fence cites nothing - put it on a source line
under the block (`<source-word>: [^cite-id]`, in the KB language), where it renders as a
footnote instead of travelling with the command when someone copies it.
- **A source page may cite another source page**, the same way any page does: `cite add --page
"Source - A" --source "Source - B"` writes `Source - B` into A's `sources:` and leaves B
untouched. A citation between two sources has a direction, and `cite add` is the only command
that records it - `xref link-source` refuses a target that is itself a source page.
- A source cited inline must also appear in the page's frontmatter `sources:` list - except a
source page's citation of itself, which never does; `wikitool lint` checks this in both
directions, and hard-errors on a leftover pre-migration
`^[[...]]` marker, an undefined `[^cite-id]` reference, or an orphaned Footnotes definition.
`tools/wikitool cite sync` reconciles a page's block after a prose edit changes which ids are
actually referenced.
- `tools/wikitool xref link-source --source "Source - X" --entities A,B,C` adds a new source
to every page it backs in one pass. Its targets are the pages the source *mentions*, never
another source page.
- Every raw file is expected to be claimed by some source page;
`tools/wikitool sources coverage` lists the ones that are not.
If no raw file or existing page backs an answer, say so explicitly rather than synthesizing
one - and never file the synthesized version back into the wiki.
## Pages that leave the wiki: guidelines
`tools/wikitool export guidelines` renders a selection of pages into one generated
`GUIDELINES.md` and, behind the Guideline Push Gate, writes it into the captured repositories that
opted in ([raw/CONTRACT.md](../raw/CONTRACT.md#getting-a-repository-in-raw-capture)). Which pages
that is, is not decided here: the stack defines no type and no field for a guideline, and the
selection - a set of `search` predicates - is written down in
[kb/CONVENTIONS.md](CONVENTIONS.md) by the instance.
What the stack does decide is how a page reads once it has left. The export is mechanical:
frontmatter, the generated links and footnotes regions and every citation marker are dropped,
`[[Title|Text]]` becomes `Text` and `[[Title]]` becomes `Title`, and code is left untouched. So a
guideline has to stand on its own in another repository - without its links to follow and without
the sources behind it - and an edit to one reaches every target repository on the next export.
The file there is never edited by hand: the next export overwrites it, so a correction goes into
the page.
## What does not belong here
- Raw source material - it stays immutable under `raw/`.
- Type definitions, frontmatter contracts, or templates - those live in `types/`.
- Procedures for operating the tooling - those live in `instructions/`.
- **Anything an instance would have to rewrite for itself** - language, naming forms, tone,
relationship labels, the hedging rule. Those are `kb/CONVENTIONS.md`'s, and a sentence
of that kind here is a sentence the stack ships over the instance's own answer.
- Rules that apply to only one collection - those belong in that collection's
`COLLECTION.md`.
- Hand-edited generated files - see [Generated files](#generated-files).
- Generated lint reports - they are written to `reports/` and are not pages.
- Answers with no source behind them.