Files
mesh-catalog/modules/mesh-knowledge/README.md
T
jochen 9f893b6305
mesh/merge-gate pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mesh-knowledge: let a record change a drafted clause, and never redraft a record whose clauses others link by number
A changes link onto a drafted clause was refused, so records that really
change such a rule could only cite it (ADR 0294, 0301, 0320); and return
renumbered ADR 0281's clauses, pointing other records' links at other
clauses. The changed drafted clause keeps its number and stays unserved;
return refuses a record whose clauses another record changed or links by
number. Issue 494, ADR 0321.
2026-10-12 14:05:37 +02:00

232 lines
18 KiB
Markdown

# mesh-knowledge — the knowledge base
The mesh's decision records and research efforts, held whole in one module (novox/hq ADR 0290, to-be 55;
the first slice, hq issue 425). One instance on the control-node holds the seat `knowledge-base`, once for the
mesh. Each record keeps its number, kind, title, topic, status, date and deciders. It also keeps its typed links,
read from both ends, and its reasoning in sections. A decision also keeps its **clauses**: each numbered statement
it makes, with its own status and source. Every version of every record is kept, with who made it, when and why.
An agent asks what holds and gets the clauses in force, the records they come from, what changed them since, and
how to read the reasoning, all in one call under 4 KiB. A record is proposed and checked when it is written, then
reviewed by the knowledge base itself, in a judgement of its own, then accepted. When its author says it needs the
operator, it is accepted only on the operator's Approve, given on a channel that proves who answered.
## The verbs (seat `knowledge-base`)
| Verb | Takes | Answers |
|---|---|---|
| `ask` | `question` (at most 500 characters), `bound` | the records and clauses bearing on the question, ranked; each clause says whether it holds, what changed it, or that it is only drafted; each record says what changed it since and how to read its reasoning |
| `get` | `number`, `kind`, `reasoning`, `section`, `version`, `bound` | one record: clauses, links both ways, what changed it since, versions, reviews, the operator's ask; its reasoning only when asked |
| `holds` | `topic` or `number`, `bound` | the clauses in force for a topic of the reading order or for one record; never a drafted clause |
| `links` | `number`, `kind` | the record's typed links, out and in |
| `history` | `number`, `kind`, `from`, `to`, `bound` | every version; with `from` and `to`, the difference between the two, line by line |
| `reserve` | `kind`, `for` | the next decision or research number, asked of the issue tracker (`issue-tracker.reserve`); spent whether or not its record is accepted |
| `propose` | the record whole | proposed, or refused naming every rule it breaks; an accepted record changes only by a progressive insight (`insight`). Each version proposed is then reviewed by the knowledge base itself (below) |
| `import` | `dry-run`, `commit` | hq's decision records and research efforts at the commit the records module stands at, each whole at its own number |
| `return` | `number`, `reason` | drafted clauses sent back to be drafted again; refused for a record whose clauses another record changed or links by number |
`ask`, `get`, `holds` and `history` stay within `bound` bytes (4096 unless asked for, at most 65536) and say what they cut
and how to read the rest. The module's own tool `knowledge_health` answers its health (below).
Its tool `knowledge_highest` answers the highest decision and research number it holds, every number spent
counted (accepted, proposed, reserved or withdrawn): the issue tracker's `reserve` starts and raises its counters
above them (hq issue 488). It refuses while nothing is held. It is a tool of the module, not a verb of the seat,
which serves the verbs ADR 0290 rule 6 names.
**Who acted** is the caller as the runtime proves it, never an argument: no verb takes one. The tool runner takes
the caller's bus user from the caller-named subject a call arrived on and hands it through the SDK
(`stdio.CallerOf`, hq issue 365). A call on the subject that names no caller (served beside it for one release, hq
issue 464), or through a runtime from before issue 365, is recorded as by `unproven`. In this module the caller is
read in one place, `callerOf` in `main.go`.
The rules about callers:
- A proposal is revised only by its author, as the runtime proves them.
- A number reserved by a proven caller is proposed only by that caller.
- A record that has a version whose writer was unproven is never reviewed, and so never accepted: who wrote it
cannot be told, and no one revises it, so its author proposes it again under a new number.
No agent reviews a record: the mesh proves which machine called, not which agent on it (hq issue 365), so the
review is the knowledge base's own (ADR 0290 rule 4).
## What a record holds
A clause is `proposed`, `in force`, `changed` (a later clause replaces part of it, named in `changed-by`) or
`superseded`. Its source is one of three:
- `written`: written as a clause by its author;
- `drafted`: drafted from a prose Decision section by the agent's judgement at the import, and never served as in
force;
- `confirmed`: drafted, then confirmed by a second, separate judgement that saw only the record and the drafts.
The links are `supersedes`, `changes` (to a clause: `0276 §4`), `extends`, `became` (from a research effort to a
decision record or design) and `cites` (to any record, an issue, or a design). Two more hold what hq's records
write and to-be 55 names no kind for (hq issue 487): `rests-on`, from a decision to one it stands on (held to the
rules `extends` is), and `touches`, from a research effort to a record, issue or design it bears on. Each link is
stored once, on the record it goes out from.
Besides the fields every record has, a decision holds `reconstructed` (true or false, where it says) and `amended`
(the days), and a research effort holds what it `touches` that is no record, issue or design ("the tool runtime")
as text. `get` serves each, and any other field a record of hq wrote under `fields`.
## Writing a record
1. `reserve` a number for its kind.
2. `propose` the record whole. It is refused, naming every reason at once, when it breaks one of the **29 rules
that judge one record or its prose**. They are hq's checks moved to the write; `PerRecordRules` in `rules.go`
lists each one with the check it comes from:
- 25 on the structure: kind, reserved number, title, topic, date, deciders, research status, at least one
clause, clause numbering, bold lead, clause size, the Context, Consequences and "How it is checked" sections,
a research effort's overview, link kind, link target, link fit, no link to itself, what it extends, what it
supersedes or changes is accepted, a changed clause is in force or changed (a drafted one too), a superseded record cited beside its
superseder, a graduated effort says what it became, and the links in its text resolve;
- 4 on the prose: no word the glossary retired, no daemon's line, a deferral names its tracking issue, and a
progressive insight in the dated, marked form.
A rule holds a record from the day hq's checks began to hold it (`RuleFrom` in `rules.go`):
- retired words from 2026-10-07 and daemons' lines from 2026-10-09 for a research effort; for a decision record
both from 2026-10-11, since hq's words check never read a decision record;
- deferrals from 2026-10-09;
- the clauses from 2026-10-11;
- the decision record's sections and the live-citation rule, which no hq check held on a decision record,
from 2026-10-11, the day the knowledge base takes the write.
A progressive insight keeps its record's date, so a correction to an older record is not refused by a rule
that never held it, and it keeps the links the accepted record holds. A new record is dated no earlier than the day its number was reserved.
3. Revise it by proposing again; every version is kept.
4. The knowledge base reviews every version proposed itself. It asks the agent module's judge
(`claude-code.claude_code_judge` on this node) one question, in a separate call with no tools, that holds only
the record, the records it links and the rules, and answers `approve` or `changes` with findings. The rules it sees
name every kind of link `propose` accepts, from the one list `propose` holds a link to, and the record it sees
shows its state in the knowledge base apart from a research effort's own status (hq issue 492). The background
work asks it at once, before the next step of any drafting, one judgement at a time. Every judgement asks a bound
sized to its question: 25 seconds and 2 more per KiB, at most 120 (hq issue 508: an eleven-clause record was never
reviewed in 25). A judgement not done within one bus call is answered pending with a ticket, and collected from
the same node with `claude-code.claude_code_judged`.
- On `changes` the record stays proposed, the findings kept with the version they judged (`get` shows them).
Its author revises it by proposing it again, and that version is reviewed again.
- A judgement that cannot be had (the judge unreachable, or an answer that is not `approve`, or `changes` naming
no finding) leaves the record proposed and says so on it (`get`'s `review`). It is never taken as an
approval. It is owed: its try count and when it is asked again (10, then 20 minutes later) are kept with the
version in the store and said in its history, so the background work takes it up at that time, also after a
restart of the tool runner (hq issue 497). After 3 tries the record says it was the last and its author
proposes it again to have it reviewed again; the operator is told once on the operator's channel (keyed by
record and version, cleared once the record is proposed again, accepted, withdrawn or superseded, and sent
again at the next step when it could not be sent). The module's health names the record and stays healthy:
the record waits on its author, not on the module, so it never holds the module's own rollouts (hq issue 508).
An ask of the operator that cannot be made counts as a try the same way.
- A review whose outcome the database refused to keep is held back for that record alone (10, then 20
minutes, 3 tries, logged as given up), so it never blocks the records behind it.
- A progressive insight waiting for its review is revised only by its proven author.
- A record revised while it is judged is not decided on the version judged; the newest is judged next.
- On approve the record and its clauses are accepted and in force. Every clause it changes moves to
`changed`, and every record it supersedes moves to `superseded`, each as a new version of that record. A record marked `needs-operator` is first asked of the
operator (an ask that cannot be published is taken back, and the record is reviewed again later): an ask with
Approve and Decline, both at the level approve, each bound to this version's digest. It is
accepted only on the router's warrant for Approve, read back from the router's own record and held to the ask
composed again. A record changed after it was asked finds the digests differ, and nothing is accepted. The
warrant is kept with the record.
5. After every acceptance, the **4 rules that judge the whole record set** run:
- `cited`: a decision accepted here is cited by another record or names a home in hq;
- `supersession`: both ends of a supersession agree;
- `partial`: both ends of a change agree;
- `rests-on-live`: no accepted record extends a superseded one.
A failure is kept as a finding and told to the operator once, as a warning naming the rule and the records
(`operator-channel.notify`, keyed `knowledge.finding.<rule>.<records>`, sent again at the next step when it
could not be sent, and cleared once the rule holds for them again). It is a fault of the records, which their
authors repair, not of the module: the module's health names it and stays healthy, so it never holds the
module's own rollouts (the operator's decision of 2026-10-12 on hq issue 504). The acceptance stands.
`cited` is also judged ahead, in the review: a decision that names no home and that no accepted (or
superseded) record links is not judged and not accepted. Its review, by the knowledge base itself, asks changes
naming the rule (hq issue 504). It is not refused at propose, because no record can link a number only
reserved: a record citing a new one is proposed after it and accepted, and the new one is proposed again.
## The import
`import` reads hq through the records module at the commit its checkout stands at. With `commit`, it refuses a
checkout standing elsewhere. At a commit where hq holds no decision record and no research effort (the records
have left it, hq issue 426), it refuses and says so, rather than answering "nothing created" as a success. It writes every decision record and research effort whole, as version 1, naming the
commit and path it came from. It creates only what is absent and never overwrites. It never asks for a number, so
no counter moves. A second run changes nothing and says so. `dry-run` writes nothing. From a terminal,
`mesh-knowledge dry-run <hq checkout>` gives the same dry run over a checkout on disk, against an empty knowledge
base.
**`refresh`** (the import's third argument) repairs what an earlier import lost or misread (hq issue 487). It
takes a record held whose every version was written by the import, a refresh, or the knowledge base's own
drafting and confirming of its clauses (nothing waits over it), reads it again at the commit and, when what it
holds differs, writes it as a new version: "refreshed from hq at <commit>: <path>", naming each field that
differs. It keeps what the knowledge base made of the record: written clauses with their statuses when hq writes
the same ones, drafted or confirmed clauses (and their drafting) while the text they were drafted from is what hq
says (else they are drafted again), and the clause a change of hq's names once judged. A record anyone changed
after its import (returned, corrected, accepted over) is never touched, and its row says so. A record read again
and found the same is left as it is, so a second refresh changes nothing. With `dry-run` it says, record by
record, what it would change.
A decision whose Decision section's own top-level list writes numbered statements with a bold lead gets them as
`written` clauses. A numbered list inside a `###` subsection of the Decision section is that subsection's prose,
never the record's clauses (ADR 0259, 0272, 0273, 0278 and 0288 number steps so; hq issue 487). A decision with no
such list (245 of 278 at hq f0892019, 196 written clauses in the rest) gets none at the import, and so does one
with no Decision section at all (11): it is drafted from what it decides across its sections, all but its
preamble, consequences and references. One too large for one judgement (48 KiB) is marked as having no clauses,
and `get` says why. A background worker then takes one
judgement at a time (`claude-code.claude_code_judge` on this node): it drafts the clauses, then confirms them in a
separate call. A clause the second judgement rejects stays `drafted`, with the reason. hq's `supersedes-in-part`
named a record, not a clause: which clause it changes is judged and then confirmed the same way, and only then is
the older clause marked `changed`. When every prose record of a topic has been through both judgements, the
operator is told once (`operator-channel.notify`, a warning) with the counts. `return` sends a record back to be
drafted again.
A drafted clause may be changed by a later record (`changes 0281 §9`, novox/hq issue 494, ADR 0321): on its acceptance the
clause is marked `changed` with the change named, keeps its number and stays `drafted`, so it is still never served
as in force, and `get` says both. A redraft numbers a record's clauses anew, so `return` refuses a record any of
whose clauses another record changed or links by number, served or proposed, naming each link: a link never comes
to point at another clause.
## Where it keeps its records
In a database in the store: the module requires a `postgres-database` provision named `knowledge`, as
`claude-licence-manager` does, and reaches it through the address in `${dir:state}/database.url`. Ranked search is
the database's full-text search (`ask`). The schema is numbered migrations (`cmd/mesh-knowledge/migrations/`), and
the consolidated schema for a fresh database is `schema.sql`. A test holds the two equal on a real postgres.
## Its processes
- **the bundle**: the verbs, `knowledge_health`, the operator's answers heard, and once a minute (and at once when
a record is proposed) the router's record read, reservations never proposed withdrawn, the reviews of proposals,
and the drafting worker; and it tells the whole-set findings. It never changes the schema, and
refuses to serve while the schema is behind.
- **`prepare`** (run once): brings the schema to this version.
- **`keep`** (judged by its unit): judges the whole record set every five minutes and keeps the findings.
- **`watch`** (judged by `knowledge_health`, run as the machine's account, the one the runtime runs the bundle
as, so the tool can read what it writes): every minute, reads whether the database serves and what the
findings say, and leaves its verdict in `${dir:state}/health.json`. That is the file the health tool answers
from. A verdict older than three minutes is unhealthy; a whole-set finding is named in it but leaves it healthy.
## Settings
| Setting | Default | What it does |
|---|---|---|
| `judge-model` | `sonnet` | the model the review, drafting and confirming judgements ask |
| `drafting` | `on` | `off` stops drafting and confirming clauses |
| `reserved-for` | `168h` | how long a reserved number waits for its proposal before it is withdrawn |
## What it needs at release
- assignment to the control-node, and a postgres provider for its `postgres-database` provision;
- the issue tracker serving `issue-tracker.reserve` (hq issue 424, merged as mesh-catalog#204) on the mesh;
- the runtime proving the caller (hq issue 365: mesh-tools#27 and mesh-controller#224 delivered), or every
proposal stays unreviewed;
- the agent module on the control-node holding a licence, or every review waits and says so;
- the import run once, then checked against hq record by record (hq issue 426).
## Tests
`go test ./...` in this directory. The store tests run over memory and over a real postgres. That postgres is
either `MESH_TEST_POSTGRES` or a private server the tests start from the machine's `initdb` on a socket of its own,
listening on no port. A machine with neither skips the postgres half and says so. The fixtures are real hq files:
`testdata/hq` holds eleven records at the commit in its `COMMIT` file, and `testdata/hq-main.tar.gz` holds the
whole of main at `testdata/hq-main.commit`. The bus test starts the bundle as the runtime does and plays the
runtime over a real NATS server.