Skip to content
RIrecord-index
455 checks · 2 consumers · stdlib only · 4 known defects, pinned

Query the record instead of reading it.

A governed SQLite+FTS5 map over a markdown decision record, so a session reads the forty lines the query pointed at rather than the six hundred it would have skimmed.

The markdown stays canonical. The index is derived, regenerated on every fold, gated by a four-leg verify, and wrong by definition the day it is hand-edited.

A repo declares what its documents mean

docs/index/conventions.json 52 required fields

A consumer binds in four lines

B = record_index.bind(__file__) globals().update(B.exports())

Then asks the record

q "dangling pointers" verify # four legs, all must pass

What it does

The record becomes queryable

Rulings, laws, experiments, handoffs, artifacts, phenomena and decisions are parsed out of a repo's own markdown into SQLite with an FTS5 index over it. Every row carries the file, the line, a human anchor to cite, and a locator string findable in the source.

Conventions are declared, not guessed

Which files carry rulings, which header forms open one, what the verdict vocabulary is, which corpora exist — a repo states all of it. There is no "if the repo did not say, use the first repo's value" branch anywhere in conventions.py, on purpose: that branch ships one repo's history as every other repo's silent default.

Every vocabulary reports what it dropped

An empty table and a table that silently discarded six artifacts are indistinguishable at the call site, and only one of them is correct. Each vocabulary declares a probe defining its population, and counts the inputs inside it that matched no entry — reported, never gating.

The numbers, as measured

Measured on 2026-08-11 against the tree at that date, not estimated. Where something has not happened, the row says so rather than being left off.

CounterAs of 2026-08-11
Checks in the suite455 — 451 passing, 4 pinned as xfail(strict=True)
Interpreters in CI3.11 and 3.13 — the floor requires-python declares and the ceiling the classifiers claim, and nothing between them
Runtime dependencies0. sqlite3 + re + json, all stdlib — asserted twice, once against the project metadata and once by walking every module's import statements
Modules10, pinned by name so one added to the tree and not to the distribution is visible
Fixture record-repos2 — alpha and beta, disagreeing on the markers, the corpus root, the word for a ruling, both arc rules, the verdict vocabulary, the header forms and the location of the declaration itself
Declared conventions fields52 required; 21 of them may be declared empty, 31 may not
Consumers2 — facet, whose ~2,462 in-tree lines became a declaration plus an adapter, and armature, whose own index seeded 15/15 with 47 rulings
Known defects4, reproduced and pinned in-tree as strict-xfail tests rather than hidden
On PyPINot yet. release.yml publishes via OIDC Trusted Publishing when a GitHub release is created; nothing publishes on push

What you declare, what the tool supplies

The whole design fits in one sentence: you must declare what your repo MEANS; you may inherit how the search is TUNED.

A repo declares its meaning — in full

Every one of the 52 fields is required. Absence is an error that names the field and is raised at load time, rather than surfacing as an empty table six steps later. A repo states its own meaning; it never inherits another repo's history by omission.

The tool supplies the mechanism

Parsing, ranking, determinism, the verify legs. Nobody adopting this has evidence to tune BM25 with, and inviting them to turn knobs they cannot evaluate is worse than a default.

Every tuning value carries its calibration

bm25(8.0, 1.0), 400 candidates, 3 phrase slots — each annotated at the site with the corpus and month it was fit on. That is the difference between a default and a number somebody once measured somewhere: a reader who wants to know whether 400 means anything for their record can see that it does not yet.

Arc is identity; experiment is grouping

The schema says both rather than picking one, because keying on the E-number alone collides on seven primary keys against a real corpus where one experiment ran two ruling series. That collision was measured before it was ruled on, and the extraction halted until it was.

Four legs, and a certificate that names the bytes

Legs 1–4 gate; the declaration audit and the vocabulary report are printed with the word DIAGNOSTIC on them, because which unrecognised inputs matter is a judgement about a record and not a property of one.

1 — Determinism

Two builds from an unchanged record, compared byte for byte, with a pre-registered .dump-identity fallback for the case where SQLite's own file header defeats byte equality. The transcript names which leg held.

2 — Counts against the record's own numbering

Declared count checks grep the markdown and compare against the database; declared sequences report gaps in a numbered range. A record carrying more than a declared bound prints a completeness note — the bound stays as declared rather than silently widening.

3 — Zero dangling pointers

Every row in all seven tables plus the FTS index must name a file that exists and a locator string that occurs inside it. This is the leg that catches a pointer the tool invented.

4 — The seeded question set

A set of questions declared by the repo, each with the file and anchor that should answer it. The gate is that the target lands within the top N; a miss prints what came back instead.

The certificate is written by the same verb

build_and_certify builds, verifies in-process, and writes the certificate from that verify's own transcript and exit code. There is no path that writes a database without writing a certificate for it — because build and verify as separate verbs let a fresh database sit beside a stale certificate indefinitely, reading as verified.

Staleness warns; it does not refuse

The certificate carries the index's size and digest and a per-file manifest of the corpus, so a certificate found beside a different index is detected rather than trusted. A corpus that has moved since the build reports STALE and keeps serving, because bounded staleness is the normal state of a fresh clone.

What is not solved

Four defects, each reproduced and pinned in the suite as an xfail(strict=True) test — which means the day one is fixed, the suite fails until this page and that test are both updated. None affects the two current consumers. A page that only lists wins is not a status report.

DefectMeasuredWhere
verify() reports its diagnostic counts twiceOn alpha, one build reports verdicts 4/3, artifact kinds 5/1, law paid_for_by 2/1, experiment status 2/1, phenomenon markers 1/2, ruling headers 6/0, 8 total unrecognised and 1 declaration finding. The transcript reports 8/6, 10/2, 4/2, 4/2, 2/4, 12/0, 16, and the same finding listed twice — exactly double, every row.index.py — Record.record() hands back a fresh Record per call precisely so counters do not accumulate, but verify() passes one Record to both of leg 1's builds. The accumulation is in the two REPORT-ONLY sections; every gating leg reads the database and is unaffected, and the run still exits 0.
The claim-arc pattern assumes E-numbered arcsOn beta, whose arcs are A01 and A02, both declared-family sites land in the unparseable list with "no arc attributable on this line", the STALE count is 0, and the claim that one of its own documents is wrong by five is never made.claims.py — ARC_RE is the module constant \bE(\d\d)\b, hard-coded from the repo this was extracted from, while every other arc-shaped value in the package is declared. measurements() carries the same assumption in CAST(substr(id,2) AS INTEGER).
The sub-ruling locator is not derived from the declared header formWith headers.sub_ruling declared as ^\*\*(\d+)\.([a-z])\s+[—–-] and the document written **1.a — , the row's locator comes out **1a , which does not occur in that document. Verify leg 3 then reports dangling pointers produced by the tool rather than by the record.parse.py — the locator is built as the literal '**%s%s ' % (num, letter), the form of the repo this was extracted from. Same for sub_closure.
Four fields refuse declared-emptinesssweep.current_state_dirs, sweep.historical_dirs, headers.handoff and vocabularies.supersede_verbs are not in MAY_BE_EMPTY, so a repo whose honest answer is "none" cannot say so — the loader refuses []. Both fixture repos carry a directory they would otherwise not have, for exactly this reason.conventions.py — the file's own law is that declaring a corpus empty is a statement where omitting the key is not, and MAY_BE_EMPTY grants that to 21 fields. These four are outside it.

The surface a consumer takes on

No console script ships with this package, deliberately: the command a record repo runs is named after that repo, so the entry point belongs to the consumer and this package supplies the contract it runs under.

The adapter a consuming repo writes

import record_index
from record_index import cli as _cli

BINDING = record_index.bind(__file__)
globals().update(BINDING.exports())


def main(argv=None):
    return _cli.run_contract(
        lambda a: _cli.main(BINDING, a), argv,
        db_env=BINDING.conv.db_env)

The exit-code contract it runs under

0  ok
1  the operator's invocation was wrong
2  the tool broke on something it did
   not expect
3  declared and DELIBERATELY UNUSED —
   no verb has a partial-completion
   path, and a code is not populated
   by inventing a path for it
4  the tool ran correctly and is
   telling you not to proceed