Development record
The consistency pass at commit time: a stable finding key, cross-document lint codes, chunked review, and a pre-commit gate
Summary
This spec delivers itd-2609291923559186: the five follow-ups itd-48 left to the consistency pass (A1, A2, A3, A4, G1), planned together under ruling BT2 and tracked until now by iss-2609260926323349.
Four pieces land in the order decision 7 and decision 6 set:
- G1 gives every finding a deterministic key,
cfk-and sixteen hex digits, derived from the class, the two ends and their collapsed quotes. The key is written into the issue a finding files (finding_ref:), and a finding whose key a resolved or won't-fix record carries is markedaddressedand files nothing. - A1 adds three record-lint rules at warn, read out of the
record_schemascan asstale_edgeandedge_cyclealready are. They name abuilds_onthe target never mentions back, arouted_fromthe source never acknowledges, and arelated_issuesentry the issue does not link back. - A4 measures the assembled corpus against a request limit, reports an overflow with its size, and reviews an overflowing corpus in chunks chosen so that every pair of documents shares at least one chunk. One report covers every chunk.
- A3 and A2 together are one verb,
abcd intent consistency precommit, run as a gate in the core-owned managed pre-commit gates. It runs record-lint's kind rules and the A1 rules over the whole corpus, and runs the model pass over the staged records and their neighbours wherever the owner's own machine configuration routes the auditor to a provider. It then acts on what the commit introduced as the repository chose at setup: warn, stop or automatic. Where no route serves, the records are marked "deep check owed" on the status board and the commit proceeds. This piece waits for the managed gates (iss-84 with itd-62, ruling J23).
Everything below is grounded in the code as it stands:
internal/core/intent/consistency.go (emit and ingest),
internal/core/capture/consistency.go (the filer), internal/core/lint
(record_schema, edges.go, config.go), internal/core/ahoy (the value
questions and their gaps), internal/core/oracle (routes, Admitted, ruling
DR5 in selfcontained.go) and internal/surface/cli/board.go.
Scope
In:
- a new leaf package,
internal/core/record/findingkey, which derives the key; internal/core/intent: the key on every finding and row, theaddressedstate, thestagedscope, overflow measurement and chunked emit and ingest;internal/core/capture: the key lookup acrossopen/,resolved/andwontfix/,finding_refon the filed record, a dry classification for the gate, andLinktaught to appendrelated_intents;internal/core/issueschema:finding_refjoinsKnown;internal/core/lint: the three A1 rules,finding_ref's shape check inrecord_schema, andrelated_issuesandrouted_fromread by the scan;.abcd/record-lint.json: the three rules at warn;- a new package,
internal/core/consistencygate: the gate's decisions; internal/core/ahoy: the setup question and its gap;internal/surface/cli: the verb, the multi-payload ingest, and the board line;- the plugin pages
commands/intent.mdandcommands/ahoy.md; - the regenerated surface snapshot and command reference.
Out, as the intent draws it: Role 1 and Role 3 beyond its scheduling; any change
to the classes or the rubric (consistencyClassText and
consistencyRubricRules are not edited: a chunk's manifest is that chunk's,
so the existing rule that each end is a manifest document already bounds a
chunked finding); and the managed pre-commit gates themselves, which this spec plugs
into and does not build.
Approach
G1: one key, derived once
findingkey.Derive(class string, a, b End) string, where End is
{Path, Quote string}, is the only derivation. Its steps:
- Each end's path becomes its identity path. Under a record store whose
folder is the record's status (the intent buckets, the issue status
folders,
specs/openandspecs/closed), the status segment is dropped:.abcd/development/intents/planned/itd-N-slug.mdbecomes.abcd/development/intents/itd-N-slug.md. Any other path, such as a brief page, is kept as it is. - Each quote is collapsed with
strings.Fields, the samecollapseSpacethe ingest already applies. - The two ends are ordered by identity path and then by quote, which is the
order
validateConsistencyFindingsalready sorts them in. - The result is
"cfk-"followed by the first sixteen hex digits of the SHA-256 ofabcd/finding-key/v1, the class, and the two ordered (path, quote) pairs, with NUL between every field.
The package sits under internal/core/record beside match because both
intent and lint call it, and lint cannot import intent.
validateConsistencyFindings replaces its local
f.Class + "\x00" + k0 + "\x00" + k1 with the derived key, so the duplicate
check and the row key are one computation. ConsistencyFinding gains
Key string (json:"finding_ref": a "key": "cfk-…" member is flagged by a
default gitleaks as the record field was, so the JSON row spells it as the
record does).
ConsistencyRow gains State (filed | linked | addressed) and
AddressedBy (the record's id and its folder). Linked stays, so --json
consumers keep working. ConsistencyIngestResult gains
Addressed []string. ConsistencyFiling gains Addressed bool, and its
IssueID names the record either way.
The rendered report gains a key column. The ledger cell reads
iss-N (filed), iss-N (already open) or iss-N (addressed: resolved) /
(addressed: wontfix). Each finding's detail line carries finding_ref: cfk-….
The filer in capture.IngestConsistency looks a finding up in this order:
- Every record in
open/,resolved/andwontfix/whosefinding_refequals the key. The ledger is read once per ingest, asloadOpenreads it now. An open record wins and links. Failing that, a resolved or won't-fix record makes the findingaddressed: nothing is filed and nothing is written to that record. - Failing that,
openRecordHolding, the existing prose match. It still links a record filed before keys existed. Linking writes nothing, as it does today, so no key is back-filled into a legacy record. - Failing that, the finding is filed.
CaptureRequestgainsFindingRef, which capture writes as frontmatterfinding_ref:and refuses unless it matches^cfk-[0-9a-f]{16}$.
check skips a finding that is linked or addressed, as it already skips a
linked one. The filing-time match of itd-2609212137116617, which
adr-2609300821558671 extended, keeps working as it does. The key does not
replace it: the key joins exactly one contradiction to itself, and the match
compares prose.
issueschema.Known gains finding_ref. Without it the ledger reader would
refuse a keyed record and drop it from every capture surface. record_schema
reports a finding_ref whose value is not the key's shape, so the
committed-ledger gate refuses what capture refuses.
A1: three cross-document rules at warn
The rules are declared as stale_edge and edge_cycle are: constants in a new
internal/core/lint/reciprocal.go, a knownRules entry each in config.go,
and one call each in lint.go beside the edge rules, because they read the same
store scan, which straddles cfg.Roots. They are armed in
.abcd/record-lint.json with "severity": "warn", so a tree that passes
record-lint today keeps passing (decision 8). The scan already parses
recordParsedFields. related_issues and routed_from join the fields parsed
for the graph and are never resolved by record_schema, which keeps its own
findings unchanged.
builds_on_reciprocal: a record'sbuilds_onnames a record in the corpus whose file, frontmatter or body, never names the source by its id as a whole token (the token testcapture.namesIDapplies). The finding names both files and the rule. A target that does not resolve isrecord_schema's finding and is skipped here.routed_from_acknowledged: arouted_fromentry is<record>:<item>. The rule judges an entry only when its record half resolves in this corpus and that record carries the item label as a whole token, which proves it is the record that did the routing. It then reports that record when it never names the receiving record back.related_issues_backlink: an intent'srelated_issuesnames an issue whoserelated_intentsdoes not list that intent. The finding names both files.
Every A1 finding also carries its key: findingkey.Derive with the rule id as
the class, the two files as the ends and empty quotes. The gate needs the key
to name a finding and to tell a new finding from one already standing. The
record-lint text renders it at the end of the message, so the line reads the
same with or without the gate.
Measured on the tree this spec was written against:
| Rule | Edges | Findings |
|---|---|---|
related_issues_backlink |
46 | 0 |
routed_from_acknowledged |
5 | 0 |
builds_on_reciprocal |
184 | 136 |
The routed_from entries are all spellings from the predecessor store, which
the local spc-33 does not carry, so all five are skipped. The 136 targets of
builds_on_reciprocal never name their follower. They arrive as warnings and
fail nothing (see the open point). These are the counts when this spec was
written; when the rule landed the tree held 155 one-way edges of 217, and
those 155 are what the exemption list holds.
A4: measure, report, and read in chunks
The limit is consistency.max_request_bytes, read through layered.Config
(the repository file, then ~/.abcd.noindex/config.json, then a bundled 524288). The
intent package claims the consistency namespace and its keys
(max_request_bytes, and on_finding below), so a misspelt key is refused
naming its file. A value below 65536 is refused, and so is a value above
1048576. The upper bound is maxAgentPromptBytes, the guarded-read cap
sendRequest already puts on every input file it sends to a provider. A corpus
file above that cap cannot be dispatched at all.
The measure is the byte length of consistencyCorpusText for the scope: the
exact file the reviewer reads, so there is no tokenizer and no estimate. Today
the brief alone is about 1.25 MB. The full corpus is already over the 1 MiB cap,
so a provider-routed corpus run refuses before it sends anything, and A4 is live
rather than dormant.
When the corpus fits, the emit is exactly what it is today: one request, one
corpus file, and overflow omitted from the result and the report
(criterion 8, second half).
When it overflows, the emit groups the documents in path order, filling each
group up to half the limit with each document's framing counted. It then writes
one chunk for every pair of groups. Every pair of documents therefore shares at
least one chunk, and a contradiction between two documents is not lost at a
chunk boundary. A scoped run (itd-N, or the gate's staged scope) pins the
scope's documents into every chunk instead and pairs them with each group of the
rest, the groups sized to what the scope leaves free. A single document larger
than the space a chunk leaves is reviewed in no chunk. The emit result and the
report name it as unreviewed, with its size.
Each chunk is an ordinary request and corpus pair:
- its receipt is derived from the scope, the whole corpus's digest, the
request limit and
chunk i of n; - its prompt names the chunk and the emit's receipt (the receipt of the
unchunked corpus), so
prompt_hashbinds both; - its manifest lists only that chunk's documents, so the existing rubric rule ("each end is a document in the corpus manifest") confines a finding to the chunk with no change to the rubric.
The emit result gains overflow: {corpus_bytes, limit_bytes, groups, chunks, unreviewed} and lists every chunk's request and corpus path.
The ingest takes --findings-json once per chunk and refuses, writing
nothing, unless it holds exactly one valid payload for every chunk of one emit.
That refusal is what makes "the report covers every chunk" checkable. Findings
from different chunks that share a key are one finding, and the owning chunk's
copy is kept (see "every pair has one owner chunk" below). The report's header states the overflow (corpus bytes, limit,
groups, chunks, unreviewed documents) and lists every chunk's receipt.
On a provider route, the front door sends the chunks one after another
through sendRequest and ingests them together. On the harness, the request
block lists every chunk for the host to run.
A3 and A2: one gate, abcd intent consistency precommit
The gate is a verb, so the managed pre-commit gates register and run it as they run any gate. This spec fixes the verb's behaviour and its exit codes. The registration, the hook file and the ordering among gates belong to the managed gates (decision 6), which is why this step is blocked on them.
The decisions live in internal/core/consistencygate, which imports intent,
capture, lint and oracle and is imported by none of them. It never
prints. The front door in internal/surface/cli performs the dispatch, as
newIntentConsistencyCommand does today.
1. Setting. consistency.on_finding is read from the repository layer only.
A machine-layer value is refused naming the file, because the choice is the
repository's (criterion 4). An absent value is warn.
2. Staged set. The gate reads git diff --cached --name-only -z. It judges
the working tree, as record-lint does. The model pass's report is therefore
always marked dirty against HEAD, which is true of a commit not yet made.
3. Kind rules and A1 over the whole corpus (criteria 5, 6 and 7). The gate
calls lint.Lint with the repository's record-lint configuration, narrowed to:
intent_lifecycle, which carries the kind-and-shelf rule of itd-34;record_schema, which carries the bundle leg of itd-34;- the three A1 rules.
Each finding keeps record-lint's own severity in the output.
4. Introduced. An A1 finding belongs to this commit when one of its ends is
staged and its key is absent from the same rules run over HEAD. For that run,
the gate puts HEAD's record stores into a scratch directory with git archive.
The directory sits under the local tier, is removed afterwards, and no live
worktree is touched. A finding already standing at HEAD is reported as standing
and never stops or fixes anything. Without HEAD (a first commit), every finding
is the commit's.
5. Route. The model pass runs only where
oracle.Resolve("intent-auditor", …) gives a provider leg (OnProvider) whose
target comes from ~/.abcd.noindex/config.json and which APIConfig.Admitted accepts.
Each of the following is "no route", and the reason is named:
- a harness route;
- a repository route;
- a route
Admittedrefuses, DR5's refusal among them; - a provider
FellBackreports unreachable.
No model is called for any of them (criterion 5).
6. Model-pass scope (criterion 6). The seeds are the staged documents the
pass's corpus holds: brief pages, and intents outside superseded/. The
neighbours are the corpus documents one edge away from a seed in either
direction in lint.LoadRecordGraph. The emit gains a scope, staged, spelled
staged-<12 hex of the seed set's digest> so that requestScopeRe, the report
directory name and the receipt all accept it. Its corpus is the seeds plus the
neighbours, and every finding must have an end in a seed, as an itd-N scope
requires today. A commit that stages no corpus document runs no model pass and
owes none. A4 chunks this scope like any other.
7. Mode. The gate acts only on what the commit introduced. An A1 finding
counts when it is introduced as in step 4. A model finding counts when its row
would be filed, meaning no record holds its key and no open record holds its
prose. A linked or addressed row is printed as known.
- warn: every finding is printed with its rule or class, both ends and its key, and the gate exits 0. The model pass's ingest runs dry: it validates and classifies the rows and writes neither an issue nor a report (criterion 2).
- stop: the same report. The gate exits 1, refusing the commit, when anything the commit introduced is present, naming each finding by its key. The dry ingest writes nothing (criterion 2).
- automatic (criterion 3):
- a
related_issues_backlinkfinding is fixed throughcapture.Link, which gainsRelatedIntents, appended through the validated single-field rewrite it already makes forblocked_by. The issue is then staged; - every judgement finding is ingested for real, so each issue is filed with
its
finding_refand the report is written to the reviews shelf. Both are staged; - each change is noted, both printed and as a
Consistency-fix:trailer line handed to the managed gates'prepare-commit-msgstage, so the commit message records what the gate added; - an A1 finding with no field to write the back-link into (
builds_on,routed_from) is printed as under warn; - the gate exits 0.
- a
8. Owed. When the staged set holds corpus documents and no route serves,
the gate writes .abcd/.work.local/consistency/owed.json: each document's
path, the commit it was owed at, and the reason. The status board's
boardOutput gains consistency_owed, rendered as one line ("deep check owed:
N records — <paths>; run abcd intent consistency"). The line is omitted when
nothing is owed, and it lives on the board only, not in statusblock.Block.
An ingest whose corpus included an owed path clears that entry. The gate exits
0 on this branch, so a judgement it could not obtain never refuses a commit
(decision 3, criterion 5).
Output. Every source reports in one run, in one envelope (criterion 7):
lintholds the kind-rule and A1 findings, each with its key andintroducedorstanding;passholdsranorowed, with the reason, the report path and the rows;modeandactionsay what the gate did.
--json emits the same envelope. A fault, such as a tree the scan cannot read
or a configuration refusal, exits 2 and leaves the managed gates' fail-closed
contract to decide. Exit 1 is only the stop refusal.
Setup. ahoy gains the value question consistency_hook, with:
- a
promptHelpentry giving the three answers in plain words; - a choices set
warn | stop | automatic; - the flag
--consistency-hook; - the gap
config.consistency_hook_missing, raised in a managed repository whose.abcd/config.jsonlacksconsistency.on_finding.
apply writes the answer there (criterion 4). It ships with this step, so no
repository is asked about a gate it cannot yet run.
The surfaces
commands/intent.md documents:
- the key and
addressed(step 1); - the overflow and the chunked ingest (step 3);
- the
precommitverb, its modes and the owed line (step 4).
commands/ahoy.md documents the new question. The surface snapshot and the
generated command reference are regenerated in each step that adds a flag or a
verb.
How each acceptance criterion is met
builds_on_reciprocalandrelated_issues_backlinkread both records from therecord_schemascan and name the rule and both files. No model runs.- Under stop, an introduced finding exits 1 naming its key. Under warn, it is printed by its key and the gate exits 0. The dry ingest writes nothing in either case.
- Under automatic, the gate adds the missing
related_intentsentry throughcapture.Linkand stages it with a printed note and a trailer. It files the judgement finding with itsfinding_ref, stages both, and exits 0. - The
consistency_hookquestion is asked byahoy(gap, prompt help, flag) and the answer is recorded asconsistency.on_finding. - With no admitted machine-layer provider route, no model is called, the A1
and kind rules run, the gate exits 0, and
owed.jsonfeeds the board's "deep check owed" line naming the records. - With a route, the
stagedscope's corpus is the staged corpus documents plus their one-hop graph neighbours, and the lint half runs over the whole corpus. - Kind rules, A1 and the pass report in one invocation and one envelope.
- The emit measures
consistencyCorpusTextagainst the limit. On overflow it reports the size and the chunk plan, and the ingest refuses unless every chunk is present. A corpus that fits emits one request with no overflow. findingkey.Derivereturns the same key from the same class, identity paths and collapsed quotes on any run. Capture writes it asfinding_ref, and the second run's row carries it.- The key lookup finds a resolved or won't-fix record carrying the key. The row
is
addressed, naming that record, and nothing is filed.
Settled here, not by the intent
These are the facilitator's design calls. None changes a criterion.
- "Both end paths" is read as each end's path with its status folder
dropped. This follows the spirit of the letter rather than the letter: with
the folder in the key, a contradiction would change key when its intent moved
from
drafts/toplanned/or its issue toresolved/. That would break the intent's promise that the same contradiction carries the same key on every run. - Key format and home:
cfk-and sixteen hex digits, ininternal/core/record/findingkey. A1 findings are keyed by the same function, with the rule id as the class and empty quotes. - Key lookup precedence: an open record with the key links. A resolved or won't-fix record makes the finding addressed. The prose match still links legacy records. Linking never back-fills a key.
builds_onreciprocity means the target names the source anywhere in its file.routed_fromreciprocity is judged only when the source resolves here and carries the item label. The predecessor store's entries are therefore not misjudged against this store's spc-33.related_issuesreciprocity means the issue'srelated_intentslists the intent.- The request limit is a byte measure of the assembled corpus file, set by
consistency.max_request_bytes(default 512 KiB, bounded by the existing 1 MiB send cap). - Chunking covers every pair of groups, so no pair of documents goes unread together. A scoped run pins its scope into every chunk. A document too large for any chunk is named as unreviewed. The ingest demands every chunk.
- The gate's model-pass corpus is the staged brief pages and intents plus one hop over the record graph. A staged issue, spec or ADR seeds nothing, because the pass's corpus and judgement are itd-48's.
- "Introduced" means:
- for an A1 finding, an end is staged and its key is absent at HEAD;
- for a model finding, its row would be filed.
Standing findings never stop or fix anything.
- A model route is a provider leg from the machine layer that
Admittedaccepts. A harness route, a repository route, a refused route and an unreachable provider all mean owed, with the reason named. - An unanswered repository runs as warn, as decision 8 and the additive
impact require.
on_findingis read from the repository layer only. - Under automatic:
- only a back-link with a field to hold it is written, through
capture.Link; - other A1 findings are printed;
- the filed issue and the report join the same commit;
- the note is printed and handed on as a
Consistency-fix:trailer.
- only a back-link with a field to hold it is written, through
- Warn and stop run the ingest dry, so neither writes an issue or a report.
- "Kind rules" are
intent_lifecycleandrecord_schema's bundle leg. The setting, not record-lint's severity, decides whether the gate refuses. - The owed list lives in the local tier and clears when an ingest's corpus
includes the path. Its board line is on
boardOutputonly. - The verb is
abcd intent consistency precommit, with exit 0 to proceed, 1 only for the stop refusal, and 2 for a fault.
Agent's choices, 2026-10-12 (for review)
The building agent made these calls where the spec is silent, taking the narrowest reading that meets the criteria. Each is open to review.
- Step 1, the addressing folder.
ConsistencyFilingcarriesFolderbesideAddressed. Without it the row'sAddressedBycould not name the record's folder. - Step 1, the reader refuses a malformed key too.
issuerecord.ValidateStrictrefuses afinding_refthat is not the key's shape, as it refuses a malformedlapsed_at. Capture, the reader andrecord_schematherefore agree.record_schema's own leg names a non-blank malformed value; a blank one is left to the reader-parity backstop. - Step 1, several closed records with one key. When no open record carries the key, the first resolved or won't-fix record in ledger order addresses the finding.
- Step 1, the field is
finding_ref, notfinding_key(from review, 2026-10-14). gitleaks' defaultgeneric-api-keyrule readsfinding_key: cfk-…as a credential assignment: on 8.24.3 (CI's pin) and 8.30.1 it flagged most such lines, and it flagged the report'skey: cfk-…detail line too. Any repository running a default gitleaks would then fail on the records and reports abcd writes, which a repo-local allowlist cannot prevent outside this repository.finding_refis flagged by neither version, so the record carriesfinding_ref:and the report's detail line readsfinding_ref: cfk-…; the value keeps itscfk-shape, and this repository's allowlist for it is gone. Nothing had written the old name outside the unmerged branch, so there is no migration. One history line stays flagged, a test constant commit 268da6f4a added, and.gitleaksignorepins exactly that line by its fingerprint (commit, file, rule, line). The pin holds only while that commit survives, so the pull request carrying this step lands as a merge commit, never a rebase, which rewrites the commit's sha and leaves the line flagged in the merged history; a squash would leave no flagged line and the pin unused. - Step 1, what the key cannot see (from review, 2026-10-14). The key is
over the reviewers' quotes, collapsed for whitespace only. So the same
contradiction quoted differently on a later run (another sentence of the
passage, a longer excerpt, a reworded document) gets another key: a closed
record carrying the old key does not mark it addressed, and it is filed
again unless the prose match links it to an open record. And a
contradiction fixed and later reintroduced with the same text gets the same
key, so a resolved record carrying it marks the new occurrence addressed and
files nothing. Both are stated on
findingkey.Derive; neither is changed here. - Step 1, the duplicate refusal names the key. "finding N repeats finding M" now ends with the key the two share.
- Step 2, the exemption list.
builds_on_reciprocalreads its ownexempt_edgesfield in.abcd/record-lint.json, one<source> -> <target>entry per one-way edge. The tree held 155 one-way edges of 217 when the rule landed (the spec measured 136 of 184), and all 155 are listed. A test holds every entry to the set it landed with, a testdata file the test pins at exactly 155 entries and by its sha256 (from review), so the list grows only by editing the Go test itself. Every entry must still name abuilds_onedge in the tree, so a dropped edge drops its entry. The test does not demand the edge stay one-way, so answering one never fails a test. - Step 2, where a finding sits. On the file that declares the link, at the
field's line, naming the other file. The key ends the message. It is also the
lint finding's
Keyfield (finding_refin JSON, as the record, the report and the consistency rows spell it, since a default gitleaks flags"Key": "cfk-…"), which no front door prints yet: a seam for step 4's gate. - Step 2, one
routed_fromfinding per routing record. Entries routed from one record share both files and so one key, and are one finding. - Step 2, the item label. A label counts only bounded on both sides by a character that is not a letter, digit or hyphen.
- Step 2, naming a record id is one test (from review, 2026-10-14).
record-lint's A1 rules and the consistency filer's prose match both call
recordid.NamesID. It reads both neighbours of the id as characters, not bytes: a letter or digit after the id, ASCII or not, means the text names another word (itd-4abc,itd-4é,itd-4sdo not name itd-4), and a hyphen after it still names it (the record's own slug); a letter, digit or hyphen before it makes a longer word. For the filer this is a change on both sides: it tested only for a digit after the id, soitd-4slinked, and it read the byte before the id as a character, so a non-ASCII character there such as›blocked a match that is now made. - Step 2, what is judged.
builds_onis judged from every store and bucket, superseded records included.related_issuesis judged on intents only. A record naming itself, or a target named twice, is no extra edge. Where two files claim one id, the first in scan order answers. - Step 2, the citation rule keeps its coverage.
related_issuesandrouted_fromjoin the graph fields.prose_citation_resolveskeeps reading both as prose, so moving them takes no coverage away. - Step 3, the limit's home. The emit takes the limit as
MaxRequestBytes, 0 meaning the bundled 524288. The 64 KiB–1 MiB bounds are the configuration loader's, so the core plans under any positive limit.on_findingis claimed beside it now and left unread until step 4. - Step 3, an unreviewed document's size is its framed size: its manifest line, its BEGIN and END lines and its text. That is the number compared with a group's budget.
- Step 3, "the space a chunk leaves" is a group's space. A whole-corpus chunk is two groups, each filled to half of what the header leaves, so a document over half cannot share a chunk with every other document. Such a document is named as unreviewed, even when it alone would fit a chunk. The emit and the ingest each warn on stderr naming every unreviewed document and its size (from review), since both still succeed and the report alone would otherwise say so. At the bundled 512 KiB no document in this tree comes near half. At the 64 KiB floor, brief pages over about 32 KB go unreviewed. Pairing such a document with re-packed groups, as a scoped run pins its scope, is left for review.
- Step 3, the header allowance is an upper bound, rendered at worst-case number widths. Every rendered chunk is still checked against the limit, and a chunk over it refuses the emit with nothing written.
- Step 3, nothing fits. When no group can form, the emit is refused rather than writing no chunk. So is a scope too large to sit in a chunk by itself.
- Step 3, the top-level paths. On an overflow,
request_pathandcorpus_pathare empty,chunkslists every chunk's pair, andreceipt_idis the emit's receipt. The unchunked corpus file is not written. - Step 3, the ingest recomputes the plan from the chunk request's
limit_bytesline, whichprompt_hashbinds, not from the configuration. A limit changed in the configuration between emit and ingest therefore never breaks a chunk set. The chunk receipt binds the limit too (from review: it first left it out, as the spec then spelled it, so a re-emit at another limit that planned as many chunks overwrote the set in flight under the same receipts). A re-emit at another limit now writes a separate chunk set beside the first, and either set ingests. - Step 3, extra refusals. The ingest also refuses:
- a payload and a chunk set handed in together;
- two unchunked payloads;
- chunks read at different commits;
- a chunk request the corpus no longer plans, or whose corpus now fits its limit.
- Step 3, the merged payload digest is taken over every chunk's payload digest in chunk order, so the same set given in any order is a noop.
- Step 3, several verifiers. When different verifiers answered the chunks, the report's verifier line lists each one.
- Step 3, every pair has one owner chunk (from review). A whole-corpus
plan holds two documents of one group in several chunks, so one
contradiction could be filed once per chunk under different quotes. Every
pair is therefore owned by one chunk:
- a cross-group pair by the chunk holding both groups;
- group i's own pairs by the chunk pairing it with group i+1, and the last group's by the chunk pairing it with the group before;
- in a scoped run, the scope's own pairs by chunk 1.
A chunk's prompt names the sets whose pairs it does not report. The ingest
sets aside a finding from a chunk that does not own its pair, and the
report and the result count it (set_aside). A key names one pair, so
findings sharing a key keep the owning chunk's copy, and no separate merge
by key is needed (from review). The
rubric is unchanged.
- Step 3, a ceiling on chunks (from review, 2026-10-14). A whole-corpus
plan grows with the square of its groups: this tree's 2.36 MB corpus plans 45
chunks at the bundled 512 KiB, 210 at 256 KiB and 2,080 at the 64 KiB floor,
and the review estimated that a cloned repository with a 20 MB brief of small
pages would plan about 200,000 and exhaust memory, because the emit rendered
every chunk before writing any. Three changes:
- a plan of more than 128 chunks (
MaxConsistencyChunks) is refused with nothing written, naming its count and the smallest larger limit, doubling up to 1 MiB, that fits the corpus in one request or plans within the ceiling; when none does, it points at a scoped run, whose chunks grow with its groups alone. 128 is about three times this tree's plan at the default, so a corpus 1.7 times today's still plans at 512 KiB and one of about 8 MiB at 1 MiB; at the default limit it bounds a run at about 64 MiB of requests; - the emit measures every chunk first, keeping no text, then renders and writes one chunk at a time, so it holds one chunk in memory, not the set, and a chunk over the limit still refuses with nothing written;
- on a provider route the verb states the chunk count and their bytes before
the first send, and a plan of more than
--max-chunks(default 8, about 4 MiB at the default limit) is refused before anything is written or sent, naming the count to pass. The emit result gainschunk_bytesand each chunk itsbytes. The cap and the bytes it names count only the chunks still to send, not those a kept answer serves (from the second review).
- a plan of more than 128 chunks (
- Step 3, the provider route. The
routemember is the first chunk's dispatch receipt, andchunk_routeslists every chunk's. If any send falls back to the harness, every chunk is left to the host, and answers already received are not ingested, because the report covers every chunk or none. They are not lost (from review): each chunk's answer is kept under the local tier as it arrives, at<chunk receipt>.findings.jsonwith the call's receipt beside it at<chunk receipt>.route.json. The fallback names the kept files, which a host ingest takes as those chunks'--findings-json, and the next provider run sends only the chunks without a kept answer that validates against its request and the corpus as they stand, reporting each reused chunk's route from the call that answered it. A kept answer is reused only when that call went to the connection and the model the route now names, and a set's kept answers are removed once the set is ingested, by the provider route or a host ingest (from the second review): without both, a rerun on another provider reused every earlier answer and sent nothing, so whether a rerun asked the model again depended on the corpus's size. Reuse is bound to the connection, the provider and the model the call went to, not to the endpoint or the settings sent, and it is judged before this emit writes its request pair, against the request the kept answer was given for. No--freshflag: a kept answer is mostly the case reuse is for, a fallback awaiting the next run on the same provider and model, and deleting the kept files forces a re-ask. It can outlive that case three ways, none of which files a wrong finding: the answers of a set that fell back and was never ingested stay until a set with those chunk receipts is ingested; a corpus reverted to the same bytes plans the same receipts and reuses them on the same connection and model; and two concurrent runs over one corpus can remove each other's kept answers, which costs only a resend.
Open point
None open. Both points were settled on 2026-10-02:
- Ruling DR5 and the gate's model pass needed no question, because the record settles it. DR5 keeps file-reading agents off a provider with a key unless the owner's personal list admits it, and DR5b-4 treats every remote provider the same way. So the gate's model pass runs where the owner's own settings allow it: on a local model, or on a provider the owner lists once that list carries bundles. Everywhere else the commit is marked "deep check owed" with DR5 named as the reason.
- The 136
builds_onarrivals were ruled by the technical facilitator: only new ones are flagged. The one-way edges that exist when the rule lands are recorded once as a baseline, and the rule flags only an edge added after it. The baseline is the shared, shrink-only warnings baseline that ruling BT3 plans (H5); until that ships, the rule's own exemption list holds the 155 one-way edges the tree held when the rule landed (136 when this spec measured it), and it never grows.
Footprint
- packages: internal/core/record/findingkey, internal/core/intent, internal/core/capture, internal/core/issueschema, internal/core/lint, internal/core/consistencygate, internal/core/ahoy, internal/surface/cli, .abcd/record-lint.json, commands/
- tests: the key over reordered ends, whitespace-variant quotes and a record moved between status folders; the same finding ingested twice carrying one key and
finding_refon the filed record; a resolved and a won't-fix keyed record making the row addressed with nothing filed; a legacy open record still linked by the prose match;finding_refadmitted by the ledger reader and a malformed one refused by capture andrecord_schema; the three A1 rules over a one-waybuilds_on, an unacknowledgedrouted_from, a predecessor-storerouted_fromskipped, and a missingrelated_intentsback-link, each at warn naming both files and its key; a corpus over the limit reported with its size and chunked so every pair of documents shares a chunk, an ingest missing a chunk refused, and a corpus under the limit emitted once with no overflow; the gate under warn, stop and automatic with an introduced back-link and an introduced judgement finding; a standing finding never stopping a commit; no route, a harness route, a repository route and a DR5-refused route each giving owed with no model call and exit 0; the staged scope's corpus as seeds plus one-hop neighbours; one envelope carrying lint and pass together; the ahoy question, its gap and its flag; the board's owed line and its absence from the site block
Steps
- G1: the stable finding key and the addressed state
- criteria: 9, 10
- packages: internal/core/record/findingkey, internal/core/intent, internal/core/capture, internal/core/issueschema, internal/core/lint, internal/surface/cli, commands/
- tests:
Deriveis stable over reordered ends, collapsed whitespace and a status-folder move, and differs by class; the duplicate check invalidateConsistencyFindingsuses the key; the same payload content ingested in two runs gives one key and the filed record carriesfinding_ref; keyed resolved and won't-fix records make the rowaddressednaming them, with nothing filed and the record untouched; an open keyed record links; a legacy open record links by prose with nothing written;issueschema.Knownadmitsfinding_refand capture andrecord_schemarefuse a malformed value; the report renders the key column and the addressed cell - needs: none
- landed: 5887d3fdb
- A1: the cross-document lint codes
- criteria: 1
- packages: internal/core/lint, .abcd/record-lint.json
- tests:
builds_on_reciprocal,routed_from_acknowledgedandrelated_issues_backlinkeach fire on a fixture pair naming the rule and both files with the key, and stay silent on a reciprocated pair; an unresolved target is left torecord_schema; arouted_fromwhose source lacks the item label is skipped; the rules are known tovalidateRuleNamesand land at warn, so record-lint over this tree exits as before - needs: 1
- landed: ec7156cfc
- A4: overflow measurement and chunked review
- criteria: 8
- packages: internal/core/intent, internal/surface/cli, commands/
- tests: a corpus under the limit emits one request with no
overflow; over the limit, the result and the report state corpus bytes, limit, groups and chunks, and every pair of documents shares a chunk; a scoped run pins its scope into every chunk; an oversize document is named as unreviewed; the ingest refuses a chunk set missing one chunk or mixing two emits, writing nothing; cross-chunk findings sharing a key are one row;consistency.max_request_bytesis read through the layered resolver and values outside 64 KiB–1 MiB are refused - needs: 1
- landed: ec768a4d1
- A3 and A2: the pre-commit gate and its setup question
- blocked: waits for the core-owned managed pre-commit gates (iss-84 with itd-62, ruling J23); steps 1 to 3 do not
- criteria: 2, 3, 4, 5, 6, 7
- packages: internal/core/consistencygate, internal/core/intent, internal/core/capture, internal/core/lint, internal/core/ahoy, internal/surface/cli, commands/
- tests: stop refuses an introduced finding by its key and warn prints it and proceeds, neither writing an issue or a report; automatic adds the
related_intentsback-link throughcapture.Link, files the judgement finding with its key, stages both with the note, and proceeds; a finding standing at HEAD never stops; no route, a harness route, a repository route, a DR5-refused route and an unreachable provider each call no model, exit 0 and write the owed list, which the board renders and a later ingest clears; with a local route the staged scope's corpus is seeds plus one-hop neighbours and the lint half covers the whole corpus; one envelope carries kind rules, A1 and the pass;ahoyraisesconfig.consistency_hook_missing, explains the three answers, honours--consistency-hookand writesconsistency.on_finding; a machine-layeron_findingis refused; the surface snapshot and command reference regenerated - needs: 1, 2, 3
Frontmatter
| id | spc-2610021456200324 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610021456200324-the-consistency-pass-runs-where-records.md
open on GitHub ↗ · commit history ↗
References1
- itd-2609291923559186The consistency pass runs where records are written and names each finding the same way every timeimplements
mentions11
- adr-2609300821558671A reading finding is matched against the record when it is stored, reversing itd-180's warm-work-only rule
- dcn-26100200000000022026-10-02 — The consistency pass follow-ups are planned as one intent (itd-2609291923559186, spc-2610021456200324, impact additive) after two adversarial reviews and a planning interview with the product thinker and technical facilitator, asked one at a time. Rulings: A3 stays in this intent, though iss-2609260926323349's body had routed it to the shipped itd-34; A4 stays and builds its own overflow signal; the pre-commit gate runs the rule checks on every commit and the model pass only where the owner's personal settings provide a route, marking it owed otherwise, and never refuses a commit on a judgement it could not obtain; what the gate does on a finding (warn, stop, or automatic: fix the obvious and file the rest) is asked at setup; the rule checks cover the whole corpus and the model pass covers the staged records and their links; the gate waits for the core-owned managed pre-commit gates (iss-84 with itd-62, ruling J23); G1 lands first; the new A1 codes are warnings, and the 136 one-way `builds_on` edges that exist when the rule lands are baselined, so only new ones are flagged. The 10 acceptance criteria are the product thinker's. Grounds: "pursued: contradictions get into the records unnoticed and are answered twice, and unattended runs write records with no one checking them for contradictions; wrong if, after it ships, the same contradiction is still filed twice, a rule-catchable one still gets in, or an unattended run still lands a contradiction the check covers."
- iss-84Managed pre-commit gates: the interim prepare-this-repo skill offers the secrets and absolute-path pre-commit config from a private template directory outside the repo. The gate belongs to the configuration layer itself: a core-owned surface should install and maintain commit gates in managed repos, replacing the template copy. Seeded from the scaffold-repo retirement analysis; relates to the pluggable safety gate draft (itd-62).
- iss-2609260926323349itd-48 shipped the consistency pass (Role 2) and nothing tracks the five spc-33 deferrals it was the routed home for: spc-33:A1 (Role-2 mechanical half, cross-doc lint codes), spc-33:A2 (Role-2 pre-commit hook, blocking-vs-advisory policy), spc-33:A3 (Role-3 pre-commit scheduling, whose shape role now belongs to the kinds lint in itd-34), spc-33:A4 (chunked corpus review, dormant until a report shows bundle_overflow: true) and spc-33:G1 (a stable reconciliation key for consistency findings across re-runs). This record is their home until one is planned; A3 goes with itd-34, the rest are follow-ups of the shipped pass.
- iss-2610110918418459The implement loop's landing never writes a spec step's '- landed:' line, so a spec the loop landed part of still reads those steps as unlanded: a later 'spec close --remainder' carries steps that already merged into the remainder, and a fresh 'abcd build' of the same spec opens a lane for them again. Found by the fidelity audit of itd-2609212103565953 (receipt rcp-2257cc3a38d9, criterion ac-3 MET_WITH_CONCERNS); spc-2609202134338445's Progress already says the landing does not mark a non-closing lane's step.
- itd-4Nothing You Notice Gets Lost
- itd-34Intents Promote In Three Flavours — Standalone, Bundle-Member, Discipline
- itd-48`intent-fidelity-reviewer` Gains Its Cross-Doc And Kind-Classification Roles
- itd-62abcd Routes The Invisible Risks An Amateur Cannot See To A Real, Fail-Closed Gate That Wraps A Trusted Scanner
- itd-2609212137116617A new capture or draft is matched against the record before it is written
- spc-33abcd-mints-collision-proof-record-ids-across-parallel-agents