Development record
The scope operand: a reading is commissioned about something, in a closed grammar, from a committed preset
Summary
spc-69 delivers itd-199. abcd reading assemble takes a third operand naming
what the reading is about, and the assembly passes the intersection of what the
include table admits at that position with what the scope selects. A scope can
only narrow.
The presets are committed at .abcd/config/reading-presets.json and map
position to scope, so the distinctness of the four readings is a property of the
configuration rather than a hope about it. warm is cold plus a delta by
construction. The bundle carries the scope so the reading knows what it was
given; the manifest carries the effective scope, its hash, and whether the run
departed from the committed presets.
The comparative position refuses, naming the channel it lacks, rather than assembling a corpus that is not its object.
This spec rests on adr-58, adopted by the maintainer on 2026-08-31. That ADR supersedes maintainer ruling M8 and amends brief invariant 15, both of which forbade a third operand; without it this spec would not be buildable.
Scope
In: the scope operand and its grammar; the preset file, its schema and its
loader; the intersection; warm as cold plus a delta; the comparative
refusal; the scope in the bundle; the effective scope, hash and override stamp
in the manifest; the preset file in the dirty gate; the operand pin; the
definition/bundle precedence sentence in each definition; the assembler version.
Out: any route to the local ledger tier; widening beyond a position's admission; the supply regime, which stays keyed on position; the size report, which is itd-198's and landed first; a fifth position; the comparative channel itself.
Approach
The grammar, and one resolution the intent left open
A scope is one token in a closed grammar of three forms, resolved in this order:
- A record id —
itd-Norspc-N. Selects the items whose path names that record.adr-Nandiss-Nare deliberately NOT in the grammar: the include table names no decisions store and no issue store, so those tokens would validate, resolve, select nothing and end in the selects-nothing refusal at every position, while the CLI help and the generated reference advertised them. An affordance that can never work is worse than an absent one, because the operator's first move is to doubt their own invocation. - A family token — resolved as a material kind from
Kinds(). - A preset name — a key of the committed preset file.
The second form is a deliberate resolution of something itd-199 left open. The
intent says "a record family"; this spec reads that as the material kind,
because Kinds() is already a closed, rendered, version-covered vocabulary and
adding a second family vocabulary beside it would be a third name for the same
distinction. It is also strictly more capable: source, test, doc and
config are not record families and are exactly the material a scope most needs
to exclude.
Collisions are refused at load rather than resolved by precedence: a preset whose name is a kind token or a record-id shape is a configuration error, named as one. Ordering the resolution is then belt and braces rather than the rule.
The preset file
{
"schema_version": 1,
"presets": {
"cold": {
"positions": {
"widening": { "kinds": ["brief-section", "glossary-term"], "records": [], "paths": [] },
"entailment": { "kinds": ["intent-projection", "spec"], "records": [], "paths": [] },
"detection": { "kinds": ["discipline", "spec"], "records": [], "paths": [] }
}
},
"warm": {
"extends": "cold",
"positions": {
"widening": { "kinds": ["doc"], "records": [], "paths": ["internal/core/lint"] }
}
}
}
}
extends is a union, never a replacement. A preset naming extends takes
the parent's selectors at every position and adds its own. So warm cannot be
narrower than cold by construction rather than by review, and a scope added to
cold appears in warm without anyone remembering to add it twice. One level
of extends only; a chain is refused, because the guarantee is checkable at one
level and an argument at two.
paths is the one place a repository path may be named, per adr-58. The
loader refuses a path that is absolute, escapes the repository, names anything
the structural deny covers, carries a control character or surrounding
whitespace, uses backslash separators, or is . — a scope that selects the
whole repository narrows nothing, and a path carrying an escape sequence reaches
a rendered terminal line, which brief invariant 13 forbids.
The file must be known to git and must not be a symlink. adr-58 admits a
preset name at the invocation on the ground that it is committed and reviewed,
and the amended invariant 15 says so — so that has to be checked rather than
assumed. The dirty gate cannot supply it, and adversarial review demonstrated
both bypasses rather than inferring them: git reports nothing for a file it
ignores, so a repository gitignoring .abcd/ (which brief invariant 5 does for
public visibility) ran against an untracked preset and stamped the run
overridden: false; and a committed symlink is tracked while git reports
nothing when its target changes, leaving the effective configuration
permanently unreviewed and freely mutable with every gate green. Both stamped
"ran as reviewed" on an examination establishing only "git reported no
modification", which is brief invariant 16's forbidden shape. The remedy is to
make the code hold what the invariant claims, not to soften the claim.
Precisely: trackedSet reads the git INDEX, not HEAD, so on its own it
establishes "known to git" rather than "committed". The committed property is
delivered by that check TOGETHER with the dirty gate, which refuses an
added-but-uncommitted preset as an A entry. Both halves are needed — the
dirty gate alone missed an ignored file, and the tracked check alone would admit
a staged one — and saying "tracked at HEAD" would credit one line with what the
pair delivers.
The shipped preset file is itself validated. Every other test overwrites it
with a fixture, so the file the whole committed-and-reviewed argument rests on
was checked by nothing and could have shipped malformed, or with warm no
longer extending cold, or with cold degenerate at a position, with every
gate green. Three tests now load the committed file: that it parses, that the
shipped warm contains the shipped cold at every assembling position, and that
cold scopes the three positions distinctly.
Duplicate preset keys are refused. Go's decoder takes the last silently, so a second block low in the file would replace the reviewed one while a reviewer reading top-down sees the first. Against the one file whose whole safety argument is that a human read it, that is a review-evasion vector.
The comparative position has no entry, and the loader refuses one. That position refuses before a scope is ever resolved, so a preset carrying a scope for it would describe a run that cannot happen.
The intersection
A scope narrows and never widens. Assemble collects candidates exactly as it
does today, then filters:
kept := cands[:0]
for _, c := range cands {
if scope.selects(c) {
kept = append(kept, c)
}
}
selects is a union over the scope's selectors: a candidate is kept if its kind
is named, or its path names a selected record, or its path lies under a selected
path prefix. An empty scope at a position selects nothing, which is a refusal
rather than a silently empty bundle.
Because the filter runs AFTER collection, every structural property the assembler already holds is untouched: the deny, the exclusion floor, the tracked-set intersection and the dirty gate all run over the unfiltered walk. A scope cannot admit a row the table denies, and the manifest's exclusion assertions continue to describe the position rather than the scope.
That ordering is pinned by a test, and was not. Mutation review moved the
filter ahead of both gates and the entire package stayed green, which made the
paragraph above a claim nothing could falsify — the shape itd-195 says to make
executable or stop making. Neither gate could be made to notice by ordinary
means: the dirty gate's predicate is a pure function of the position, so it
refuses under either order, and the exclusion floor's own paths are structurally
denied, so no candidate can breach one to begin with. The property was real,
load-bearing and untestable at once. assertExclusionsHook is the seam that
makes it observable, and TestGatesSeeTheUnfilteredWalk fails when the filter
moves — verified by performing that move, not by asserting about it.
What the artefacts carry
The bundle gains scope, as the reading's own fact: a reader told its object is
the shipped tree and handed a tenth of it will report the missing nine tenths as
a finding, so the bundle has to say what it was given.
It is a projection, not the resolved scope, and the difference is brief
invariant 15. The manifest may name repository paths — that is its job, and it
is why the bundle can be pathless and still checkable. The bundle may not: the
assembled input is the reading's entire working set and no repository path
enters its context. A scope's paths selectors ARE repository paths, so writing
one Scope type into both artefacts put a path into the reading's working set.
It did, and the probe that found it is now a permanent guard
(iss-2608312058244357). The bundle therefore carries the kinds and the record
ids it was scoped to, plus a COUNT of the narrowings by location and never the
locations — enough for a reading to know it holds a subset, carrying nothing
about where the subset came from.
The manifest gains scope, scope_hash, and scope_overridden. Naming a
committed preset is not an override; naming a record id or a kind directly is,
because that is a departure from what was reviewed and is the thing worth
counting.
Both shapes move, so SchemaVersion goes 2 to 3 and AssemblerVersionCore
goes 1.1.0 to 1.2.0.
The definition/bundle precedence
Each definition's Object section gains one sentence: the definition states what the position MAY read, the bundle states what THIS run was given, and where they disagree the bundle governs. Without it a reading holds two accounts of its object with no rule between them, and reports the difference as a finding.
The comparative definition carries the precedence sentence too, and then a second paragraph recording that the position is not currently assembled and why. An earlier draft gave it only the second, which left ac-12's "each definition" true of three of four.
The dirty gate and the operand pin
PresetConfigPath joins LintConfigPath in the dirty set, by the same argument
already written beside it. readingOperands gains scope, which is itd-184's
pin working as designed: the addition turns it red until someone states what the
operand does, and adr-58 is that statement.
Acceptance criteria mapping
| ac | delivered by |
|---|---|
| 1 | required flag; refusal names the three token forms |
| 2 | record-id selector; intersection with the position's admission |
| 3 | filter runs after collection, so the deny and the floor are untouched |
| 4 | scope intersects admission; drafts stay denied at widening |
| 5 | Bundle.Scope |
| 6 | Manifest.Scope, Manifest.ScopeHash |
| 7 | Manifest.ScopeOverridden, false for a preset name |
| 8 | PresetConfigPath in the dirty set |
| 9 | per-position preset scopes; pinned across the three assembling positions |
| 10 | comparative refuses, naming the channel |
| 11 | extends unions, so warm ⊇ cold; pinned by a test over every position |
| 12 | precedence sentence in each definition's Object section |
Tests
Watched fail before, pass after; each guard proved by mutation.
TestScopeIsRequired— no scope refuses and names the grammar.TestScopeNarrowsNeverWidens— for every preset and position, the scoped item set is a SUBSET of the unscoped one. Mutation: a selector naming a denied path adds nothing.TestScopeCannotReachTheLedgerTier— an operator-written scope naming a local tier path yields no item from it.TestDraftsStayDeniedAtWidening— a scope naming the intent kind at widening admits no draft or planned item.TestWarmContainsCold— for every position, warm's selector set contains cold's. Mutation: makingextendsreplace rather than union turns it red.TestPresetNameIsNotAnOverrideAndADirectScopeIs.TestGatesSeeTheUnfilteredWalk— the pipeline's order, proved by moving the filter ahead of the gates and watching it go red.TestBundleScopeCarriesNoRepositoryPathandTestNoBundleFieldIsAScopeSelector— brief invariant 15 held against the scope, after the first implementation broke it.TestThreePositionsCarryDistinctItemSets.TestComparativeRefuses— and names the channel.TestPresetCollisionIsRefused— a preset named for a kind or a record id.TestUncommittedPresetRefuses— the dirty gate.TestBundleCarriesTheScope,TestManifestCarriesScopeAndHash.
Both eval lanes are run explicitly and under TMPDIR=/tmp. The read-block eval
is run at a preset that keeps every carrier: three of its eleven carriers are
shipped-tree files, and fence.go is the sole corpus behind the body-redaction
row, so a scope dropping source would turn a real assertion into an undeclared
gap.
Out of scope
- The comparative channel. Its absence is why that position refuses.
- Widening a scope beyond the table. A scope intersects and only narrows.
- Any scope reaching
.abcd/.work.local/. Brief invariant 14, unconditional.
Frontmatter
| id | spc-69 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-69-a-reading-is-about-something-narrower.md
open on GitHub ↗ · commit history ↗
References1
- itd-199A reading is about something narrower than everything, and its scope is a committed, adjustable choiceimplements
mentions10
- adr-58A reading is commissioned about something, so the invocation takes a scope
- dcn-26090200000000122026-09-02 — What a reading is handed has two committed dimensions, both the documents' own (maintainer): the OBJECT SET (the framework's term, section 13: which records and delivered paths the run is about; for Iteration 2, Iteration 1's shipped state, which here is the ledger: itd-177 to itd-189, itd-198, itd-199, spc-55 to spc-69, and the packages and pages they delivered) and the KINDS within it (the companion's four, section 4.5). A committed preset entry per position carries both plus the window it measures at; nothing at the invocation chooses either. A reading of the object set includes all kinds when the object set fits the reader's window, with the size report stating any figure over the two-hundred-thousand target; measured on 2026-09-02, the ledger at the detection position with all kinds is about 611,000 estimated tokens, which fits a million-token reader and is stated over target. A kind that proves useless leaves the default entry by a commit, which is how the record learns; a larger object set in future narrows kinds the same way. The word "object" alone stays the companion's, for what a position reads; "object set" is the run's, and "object of interest" is its plain-language gloss.
- iss-2608312058244357The scope carried in the cold-reading bundle serialised a preset's path selectors verbatim, so a repository path reached the reading's own working set and breached brief invariant 15's pathless-bundle rule; the manifest may carry paths and the bundle may not, and one Scope type was being written to both
- iss-2609290448510918The predecessor-store qualifier sweep reaches past the six intents iss-2609261536147903 named: outside drafts/ and those six, 192 citation sites across about forty intents name a spc-N at or below spc-70 that is not the citing intent's own spec, with no '(predecessor store)' on the line. Some are live cross-references (the cold-reading family's intents cite each other's live specs), and some are the predecessor store's (itd-4, itd-6, itd-29, itd-47 and itd-49 describe pre-rebuild work in its terms), so each site needs the same reading against the live spec it collides with; the specs charter's Two spc-N Namespaces section is the rule. Found while sweeping the pattern of iss-2609261536147903 in lane drainDrift3; a census script over intents/{planned,shipped,disciplines} reproduces the count.
- itd-184Four cold-reading definitions, one blindness core — each position licenses a different output, and none may hold another's licence
- itd-195A claim about how the code behaves is executable, or it is not made
- itd-198An assembly reports what it would cost before a reading is commissioned
- itd-2609020625400445A committed preset declares the window it was calibrated for, and the record holds every preset to its declaration
- spc-2609020626039834The comparative channel: one widening run's candidates, two fields each, characterised before anyone admits one
- spc-2609020626048722One committed entry per position, in three parts: the object set, the kinds within it and the window it measures at, held by an eval that re-measures every entry on every committed change