Files
mesh-catalog/modules/mesh-knowledge/README.md
T
jschoubben 1cc0ab369f
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 superseded: a newer head of the same pull request
mesh-knowledge: the import keeps every field and takes clauses only from the Decision section's own list, and can repair what it held (hq issue 487)
The import at hq f0892019 lost reconstructed, amended and a research effort's touches, held rests-on only as a
citation, took eight records' clauses from a numbered list inside a ### subsection, and left the 11 records with
no Decision section clause-less with nothing queued. Each is held now; refresh repairs a record only the import
ever wrote, and never one changed since.
2026-10-11 12:18:03 +02:00

13 KiB

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 someone who did not write it, 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)
review number, verdict (approve or changes), findings accepted, asked of the operator, or kept proposed with the findings
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

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

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 review is refused from anyone who wrote any version of the record.
  • A review is refused for a record that has a version whose writer was unproven: who wrote it cannot be told from who reviews it, so its author proposes it again under a new number.

Until the runtime proves callers, every review is refused.

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, 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. A review by someone else either approves it or asks for changes. 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 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 makes the module unhealthy, so the mesh raises a condition naming the rule and the records. The acceptance stands.

The import

import reads hq through the records module at the commit its checkout stands at. With commit, it refuses a checkout standing elsewhere. 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). For a record held whose every version was written by the import or a refresh (no one has changed it since, and nothing waits over it), it reads the record again at the commit and, when what it holds differs, writes it as a new version: "refreshed from hq at : ", naming each field that differs. A record anyone changed after its import (drafted, accepted over, corrected) 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.

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 the router's record read, reservations never proposed withdrawn, and the drafting worker. 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.

Settings

Setting Default What it does
judge-model sonnet the model the 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) before any review is taken;
  • 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.