Code records what was built and git records when it changed. Neither records why it was built this way and which roads turned out to be dead ends. This directory does.
The test for writing one: if someone (including us) looks at a piece of code in six months and asks "why not simply…", and the answer is not in the code, there should be a record.
File names are NNNN-short-english-title.md, four digits, increasing in creation order, no gaps. The number is a stable anchor for references and says nothing about priority.
The directory is flat. No subdirectories until a second kind of document appears.
Revisions stay in place. When a conclusion changes, especially because checking the code overturned it, keep a dated revision note where the original claim stood: what it said, why it was wrong, what changed it. The ledger this product keeps never updates a fact in place; a correction inserts a new row that supersedes the old one, because the change of mind is information. Records follow the same rule: knowing "we assumed a range would take effect immediately, checked, and found it never did" tells the next reader what to check first.
When too much has changed, write a new record and mark the old one superseded by NNNN at the top instead of rewriting it.
The PR that implements a record updates its status line. Three status lines once lagged behind the code in the same PR, written by the same person. So the PR description answers one question: which record does this change implement or overturn, and is its status line updated. (Added 2026-09-02, from 0016.)
Line numbers drift and file names change. Prefer function, table and constant names over file.rs:123. Migrations were consolidated from 53 files into one per domain (#130, #131); older references to migration files are by domain.
Language: English. The first sixteen records were written in Chinese and condensed into English on 2026-09-03; the Chinese originals remain in git history. Code comments are still Chinese; UI, README and records are English.
| Record | Status | |
|---|---|---|
| 0001 | Ontology import and governance | In progress · P0–P2c built; the P3 budget built, P3a by hand only; P3b built in a different shape; P4b / P4c pending; P5 delivered by 0002; criterion 2 half overturned by 0012 |
| 0002 | Reasoning engine | R0 checker and R1 materialization (KB switch, default off) built; R2 proof chain (#227); contradiction signals per 0017; R3 incremental maintenance not built |
| 0003 | The ontology grows out of the corpus | Built and running, default on · starting point rewritten by 0010 and the retired seeds · dismissal redone per 0007 · the "new phrasings" reminder pending |
| 0004 | Language follows the reader of each text | Built · UI strings, coded server errors, ontology description language and the locale of generated text · the browser language is not guessed yet |
| 0005 | The alert center | Built · five alert kinds live, search and paging in the panel · document.no_text_layer still unwired |
| 0006 | Ontology scale and the extraction prompt | Built · the character budget (24,000) and per-chunk retrieval live, values untested · the per-chunk list keeps to the same budget with first-sentence descriptions (#701) · answer keys still hand-filled |
| 0007 | Counting decides what becomes a relation | Built · adoption decided by counting (MIN_DOCS = 2, MIN_SIGNALS = 3), proposals persist (#112) · narrative verbs and _by folding still open |
| 0008 | Ontology packs as the cold start | Built · five packs embedded, multi-select at creation · no pack by default and none at registration (#580, measured) · three open questions stay open; Chinese labels got worse |
| 0009 | An undecided type stays empty | Implemented · type_id nullable, builtin classes gone · kin classes go to Review (#226), declared disjointWith keeps them apart (0016 B3) · metric / dimension builtin on demand (#231) · metric / dimension to retire under 0036 |
| 0010 | An unnamed relation stays empty | Implemented · predicate_id nullable, related_to gone, wording recovered by fact_surface_predicate · follow-ups done with 0011 |
| 0011 | A mapping is configuration | Implemented (#126 / #140 / #148) · Review flow and revision history rebuilt · the evidence chain not built · revised in part by 0036: the concept becomes an attribute or a rule, the table becomes a rendered one |
| 0012 | The ontology is a contract | Implemented · violation rate 57% → 4%, reversals 39 → 0 · guard extended to adoption and merge (#190 / #196) · reified-shell filter at pack import open |
| 0013 | A source hands over its history | Implemented for GitHub, Jira and Notion (#134 / #135 / #213) · Feishu and Confluence not started · GitHub and Jira timestamps written to the second (#691) |
| 0014 | Identity from the person, scope from the token | Implemented (#180) · five read-only MCP tools over Streamable HTTP · tokens page at /account/tokens · can_write still hard-coded false |
| 0015 | A recorded sentence waits for a nod | Implemented · memory facts wait in pending_facts, nod queue and chat card, remember reopened · MCP write is the next cut |
| 0016 | Close the open seams before cutting new ones | In progress · A done · B done (B4 deferred) · C1 done (#289) · C2 done (#297) · C3–C5 open · D2 worked around (#231); the lakehouse landed ahead of D4 (#239) |
| 0017 | A contradiction points at an error upstream | Implemented · B2a: engine and queue, per-item cap, aggregation by rule pair, cards with clues and repairs (#238) · B2b: contested edges in the alert colour, ghost edges for blocked derivations, the disputed chip and the "did not land" section in the panel (#243) |
| 0018 | The lakehouse is one protocol away | Implemented: Trino (Iceberg / Delta / Hive), Databricks and Snowflake behind the same trait, scheme picks the engine (#239) · Trino verified against a real cluster (#327); Databricks and Snowflake still want one (#241, #242) · MaxCompute waits |
| 0019 | The second clock can be rewound | Implemented in three cuts · held_at and as_of on every graph read (#317), entities' own clock by unwinding entity_merges (#337), retrieval as of a moment · the entity panel and entity_facts rewind derivations too (#549) · the control on the graph page is still open (#307), full-text recall is still "now" only |
| 0020 | An auditor reads it without us | Implemented · GET /kbs/{id}/export?format=turtle|jsonld streams the base as RDF, rdf.rs holds the mapping · SPARQL waits, and the record says why (#308) |
| 0021 | A rule reads attributes and concludes a type | Implemented (#359) · derived_facts widened to match facts, rules authored in attribute_rules from the ontology page, evaluated in the materialisation job, explained in the entity panel with their premises · read-only over MCP, writing a rule stays out · no canvas marker, and a conclusion is rewritten rather than edited |
| 0022 | An unknown date is not an open one | Implemented in two cuts (#394 and the derived cut) · world_axis predicate beside record_axis, facts.attested_at anchors a missing start or an undated end at the document that attests it, every read and both client filters on the read interval, an undated ending closes the dated row it ends, derived rows intersect premise intervals as read and carry no precision on an anchored bound · two anchors (attested_from / attested_to), so a bare open row closes too (#393) · the temporal engine orders a start-less row by its earliest dated evidence and closes its predecessor as ended-unknown there; ends the engine drew are marked and recomputed from the rows a timeline has (0057); a relative deadline is stored as written (#679) |
| 0023 | RSS observations are not documents | Implemented in #326 |
| 0024 | The world axis reaches the second | Implemented · the precision ladder runs year → second (not below: no source states less), a stored value is truncated to its precision by CHECK on all three tables, a clock time without a zone is a date, one list spells the ladder in the extractor, the renderers, the export and the evaluator; a derived bound takes the precision of the premise that set it |
| 0025 | Governance reads the ledger before it decides | Cut 1 implemented · a per-base governance switch, a govern job that works the duplicates queue first in, first out with each head's cluster, precedents pulled from the ledger into the prompt, a gate where the agent's own confidence decides and history lowers the bar or blocks (revised after the first big-file run), every look a row in agent_decisions, answers through the person's own decide path · the switch, the Agent queue, the proposal chip and the Overview section in the UI · a pair the batch cannot settle is looked at again with tools and then decided or asked about, within a daily budget · two reverts in a week turn the switch off and raise an alert · identity rules the model reads and two it cannot argue with (name shapes, type families), measured against a hand-labeled set of 411 name pairs by scripts/bench/govern.mjs |
| 0026 | A decision records why | Implemented · resolution_reviews.rationale and why on the ledger event from every human decide path, precedents quote it into the prompt and the ledger_search tool, the batch adjudicator reads precedents and keys its cache on them, the one-in-ten sample for a person was removed 2026-09-14, the model's own why is kept beside machine decisions · the impact gate (#357) and the investigating adjudicator (#358) follow |
| 0027 | An automatic merge is gated by what it can undo | Implemented · execution_gate in the store, asked by the batch adjudicator and the governor before an automatic merge: a contradiction the consistency check would open (read on the time axis since 2026-09-14: a succession is not one), a derivation resting on either side, or an answer that named either side holds the pair for a person as escalate_impact whatever the confidence · keeps are not gated · exports and a per-deployment opt-in stay open |
| 0028 | The adjudicator looks before it asks | Implemented · consequences joins the second look's tools (what a merge would touch, from 0027's gate, and whether the types share a family) · with governance off the batch adjudicator sends its unsettled pairs through the same loop under the same budget, each look a row in agent_decisions · the number that justifies or retires it comes from 0026's sample split by via |
| 0029 | A rule may say "or", once | Implemented · a group_seq on a rule's conditions (migration 0039): same group joins with and, groups join with or, one level and no nesting — because a hit carries the readings that made it true and a disjunction has no premises of its own · the combination cap is per group, two groups on one interval are one conclusion with the first group's proof, not_in joins the ops · "no such attribute at all" is deliberately not here (no premise, and a missing reading means nobody wrote it down) · the editor writes blocks and the table reads the sentence back with its "or" in it |
| 0030 | A rule may read what a rule concluded | Cuts 1–2 · A rule could not read another rule's conclusion (attribute_facts() reads facts, subject scope is entities.type_id, one pass), so A → B → C has to be written as one flattened rule. The answer is the fixed point the axiom side already runs, inside one call and in memory — which answers 0013's objection instead of waiving it, since a run stays a pure function of the asserted facts. A derived typing enters as a premise, not a filter, so the conclusion narrows to the window the entity was actually a B; a rule may read its own conclusions because the derivable space is finite; fact_derivations gains premise_derived_id and the proof becomes a tree; retirement cascades for free because recompute is total. The page showing which rules feed which is next |
| 0031 | An event holds at the moment it names | Implemented (#486) · Validity::under normalises every write by the predicate's temporal (an event is one moment written at both ends, an eternal fact has no dates), world_axis and read_span read an event as the bucket it names and an undated event at no moment, an eternal fact at every moment · the prompt marks [event] / [eternal] and says what to write · no schema change, old rows read correctly · the panel's point rendering and the ontology hint are the UI cut |
| 0032 | A rule computes what it concludes | Cuts 1–2 · A rule's conclusion was a constant (attribute_rules.conclude_value is a JSONB literal), so a criterion about a number nobody wrote down — net pay, a margin, a ratio — gets computed elsewhere and typed back in as an assertion, losing the readings it stood on. An operand on either side becomes an expression tree over attributes: its premises are the readings it touched and its interval is their intersection, so retirement needs no new machinery. Picked, not typed — not because a parsed string would break the display (it would not; that reason is wrong and recorded as wrong) but because a picker cannot compose an expression over a predicate that does not exist, and because a text box grows a grammar on request. A value may be reached across a relation (a path has definite premises and an interval; a to-many path is just a cartesian product the evaluator already walks), while aggregation is refused: a sum asserts these are all the readings, a completeness claim this base cannot hold — and while recompute still retires it correctly, its proof cannot say why it was withdrawn, and nothing incremental could ever wake it. Row count, unit checking and division by zero are the costs |
| 0033 | RSS summaries are scoped to the source being listed | Implemented · source- and generation-scoped lateral aggregation (#462), then the nested nullable rss_full_content contract replacing the eight flat columns (#417); generation and baseline_count stay internal, and baseline rows are intentionally outside the five work counts |
| 0034 | An action is a declared call | Cut 1 · the record. A base can conclude but cannot act: nothing holds a call it may make, a typed parameter, or what it sent. An action is data (method, URL, headers, a sealed auth block, a JSON body template, scalar parameters with bounds or a value set), substituted and never scripted, so the page renders what runs and an unknown placeholder is refused at save. Registered once for the deployment and granted to a base, one layer where warehouses have two because nothing is mounted. Every run is a row read by two log pages. The reach is the operator's, with no placeholder in the host. Rules fire actions in the next record |
| 0035 | A vector index is built by a job | Implemented · a partial HNSW index per dimension on chunks.embedding and entities.profile_embedding, requested by the first write of that dimension and built by a build_vector_index job outside any transaction · the two nearest-neighbour reads write the dimension as a literal, cast both sides and set hnsw.iterative_scan = relaxed_order · type resolution gathers its neighbours eight at a time in order and remembers descendant sets per batch (#512, #514) · dimensions above 2000 stay on the exact path |
| 0036 | Exploration aligns a schema to the ontology | Written, not implemented · exploration proposes an alignment per table (the class it is a table of, its attributes, its relations, a conversion tree per column) through ontology_proposals, adopted as one thing · a definition is a rule over aligned attributes, written by a person; a shared convention is one rule others read · conversions are 0032's expression tree, text parsed by sqlparser, never stored as SQL · Metric / Dimension retire and concept_mappings becomes rendered · the schema document leaves extraction · where a rule runs against the source stays open |
| 0037 | A relation carries its own attributes | Written · cut 1 in progress: qualifier tables (0049), declaration, extraction, panel, export · entity-valued qualifier reserved, conflict-as-two-rows and the canvas label next |
| 0038 | The interface has a light side | Implemented (#599) · data-theme on <html>, chosen by the reader and stored in the browser, system resolved to one of the two before first paint · a second token block in styles.css and not one rule rewritten: alpha stays, the triplet swaps · the canvas reads its colours from the tokens through one reader file and rebuilds on a switch; edges are flattened over the ground on light because the WebGL shader can only brighten · a raw-colour guard rule over .ts and .tsx · the entity palette is the same in both themes; a paper-tuned set is open |
| 0039 | A chunk is what extraction sees | Implemented, cut 1 · the parser's Markdown is read into blocks and packed by a token budget (1000, cl100k) · a table travels with its caption and header and a continuation repeats both · headings become a breadcrumb and fill chunks.heading · a page-break rule is not a boundary and a table it split is joined back · not measured on its own: the budget · cut 2: an external parser behind the block model, evidence that names a cell or a region |
| 0040 | A chunk says where its words came from | Cuts 1–3 implemented (the ledger shape, and a file that needs a reader degrades with an alert instead of becoming garbage text; scans and images read by a workspace's MinerU service, one segment per page, waiting on the service without spending retries; recordings read by a diarizing transcription model, speakers in the text and times in the anchor, an unlabelled transcript degrades) · scans and images through a MinerU service, recordings only with speaker labels (revised 2026-09-15) · a chunk carries an origin (stated, ocr, transcribed, described) and the model that produced it, and an anchor back into the original bytes (a page and region, a time range, an image inside the file) — decided before any media reader, because a transcript stored without its times can never be tied to the recording again · the packer never mixes origins in a chunk · facts from a description enter below AUTO_CLOSE_MIN_CONFIDENCE, so a misread chart opens a conflict instead of closing a correct fact · per-modality model settings, media reading as a resumable job, origin and anchor in the API, MCP and RDF · cuts: ledger shape, scans via MinerU, recordings, descriptions, video |
| 0041 | A name is a claim about an entity | Cut 0 built (identity bench) · cut 1 implemented (#670): names are value facts on known_as, the extractor reports other names, a shared name goes to the adjudicator; forward/reverse F1 0.43/0.54 → 0.68/0.68 · cuts 2–4 (name vectors and neighbours, evidence decides, re-evaluation) not started |
| 0042 | The chat loop is a runner with hooks | Implemented (#548) · the loop is rig's runner and every policy is a hook with a typed result · the wire stays LlmClient behind RigModel · a turn cannot end before a tool has run, no_evidence_needed is the exit for questions not about the base · RAG fallback only on a 400/422 to the first request with tools · an empty reply is asked again once · the skip rate is the model's (DeepSeek-V3 1–3 of 12, Qwen2.5-72B 0) and recorded, not prevented |
../pipeline.md, how a document becomes a graph. Records explain why; that page explains how things flow and where they get dropped, with five mermaid diagrams. Newcomers read it first, then come back here for the reasons. It is the "second kind of document" the conventions mention, kept at the docs/ root beside decisions/.
The docs/ root is a local scratch area (/docs/* is git-ignored except /docs/decisions/). Research notes, temporary checklists and test output live there and stay out of the repository. When a draft settles into a judgment worth keeping, it moves here as a record.