Development record
Claim typing and scope-condition identity
Summary
spc-55 delivers itd-177: the intent record's three claim kinds become machine-readable, and every scope condition gains a minted identity that survives a rewrite of its own text. The recording requirements themselves are not this spec's to invent: they are the claim-recording-gradient discipline's rule, and the two sections are adr-51's format. What lands here is the parse, the mint, the render, and the refusal.
The identity is the load-bearing half. A disposition attaches to a condition, not to a sentence, so spc-59 has something stable to key on when the fidelity verdict arrives.
Scope
- Claim parse (
internal/core/intent/claims.go, new): One reader for the## Mechanismand## Scope Conditionssections, returning the three byte states the gradient distinguishes (absent, empty, nullity token) plus the parsed conditions with their identity markers. It composes the section primitivesaudit.goalready owns (sectionBody,headingRe,bulletRe), so the corpus keeps one notion of what a section and a top-level bullet are. - Readiness gate (
internal/core/intent/ready.go): Two new checks,mechanism_claimandscope_conditions, added toReadyResult.Checksin fixed order afteracceptance_criteria.Readystays a read-only reporter: the checks report, they never stamp. - Identity mint (
internal/core/intent/lifecycle.go):Planstamps a marker onto every unmarked scope-condition bullet before it moves the record drafts → planned, and does that step alone — no spec, no bucket move — when run on a record already inplanned/, so a condition written after planning still reaches the mint and the gate's remedy is a command that works. A run with nothing unmarked refuses. It usesrecordid.Minter.Mint(the adr-45 mint). Markers are never hand-typed. - Create-path scaffold (
internal/core/intent/create.go):seedDraftgains both sections with their one-line contracts, so a drafted intent arrives already carrying the prompt rather than being told about it later. - Surface (
internal/surface/cli/cli.go,commands/intent.md): The new checks render in both the text and--jsonforms ofabcd intent ready; the plugin surface's planning interview gains the mechanism prompt and the scope-condition elicitation as named steps. - Corpus pass: The
planned/bucket is populated with conditions or the nullity token in the same change set that promotes the refusal.
Approach
The parse is one function, not four call sites. ParseClaims(content) Claims returns Mechanism ClaimState, Conditions []ScopeCondition and
ConditionsState ClaimState, where ClaimState is one of absent, empty,
nullity or stated. Everything downstream (the gate, the Plan stamp, the
--json render, and spc-59's verdict validation) asks this one reader, on the
countAcceptanceCriteria precedent: three gates already share one criterion
parser precisely so they cannot disagree about what a criterion is.
The nullity token is exact. None stated. alone on its line under the
heading, matched anchored after a \r trim, is the only spelling that reads as
a recorded nullity. Prose that merely contains the word "none" is stated, and
a heading with nothing but blank lines under it is empty. The three states
map to three gate outcomes and are never collapsed.
Decision — the marker grammar. A condition's identity is an HTML comment
carried anywhere in the bullet, written at the end of its first line:
<!-- cond: cond-<16 digits> -->. It is READ anywhere in the bullet, including
a folded continuation line, because a file wrapped at 80 columns gets rewrapped
and a positional read would orphan the identity (iss-2608300235377731). The comment
form is the repository's existing machine-marker idiom (audit.go's
<!-- abcd-review: … -->), it survives markdown rendering invisibly, and it
keeps the condition's prose exactly what a human wrote. The family tag cond
is minted through recordid.Minter.Mint("cond"), which already validates a
lowercase family before it is spliced anywhere. cond is deliberately not a
record store: the marker identifies a claim inside a record, and abcd <id>
dispatch is untouched.
Decision — the observable surface is abcd intent ready --json, not
abcd intent --json. itd-177 names the latter, but abcd intent with no
argument renders intent.Status, a corpus-wide count-and-link summary with no
per-record body; folding every intent's conditions into it would make a status
render read every file for a payload nobody asked for. The per-intent gate
already takes an itd-N, already emits --json, and is the exact moment the
identities matter, so ReadyResult gains Conditions []ScopeCondition and the
identity criteria assert against that payload.
The gate refuses, it does not repair. A missing or duplicated marker is
reported by name with the remedy abcd intent plan <itd-N> (the write-capable
verb), never silently minted at read time: a reporter that writes is a reporter
whose output depends on who ran it. Plan is idempotent here, so re-running it
on a record whose conditions were edited stamps only the unmarked bullets —
which is why the verb accepts a planned record for the stamp step and not only
a draft (iss-2608300210588874).
Identity lifecycle is a stamping rule, not a diffing engine. An edit keeps
its marker because the marker is bytes in the bullet and nothing rewrites it. A
split keeps the marker on the first part and Plan mints for the unmarked
second. A merge keeps the surviving marker; the retired one simply stops
appearing, and its absence surfaces as narrowed at the next fidelity verdict
(spc-59's business). A deletion is the same shape: the marker stops appearing,
and nothing here reads the record's history to notice — the gate judges the
record in front of it, and a retired identity is spc-59's to account for. No
text similarity is computed anywhere:
the intent asks for identity that survives rewording, and a stamped token
delivers exactly that with no heuristic to be wrong.
Discipline records are exempt. parseIntent already carries the bucket, so
both checks return OK with the detail "discipline records carry no claim
sections" for BucketDisciplines, matching the gradient's own exemption.
Staging is loud and two-step. The parse, the Plan stamp, the scaffold and
the --json render land first with both checks reporting OK and a detail
naming what is missing. The refusal is promoted in the second commit, after the
planned/ corpus pass, so the gate never arrives as a wall of pre-existing
failures. The window in which the format precedes its enforcement is stated in
commands/intent.md rather than papered over.
Acceptance criteria mapping
| itd-177 criterion | How spc-55 satisfies it | Test that pins it |
|---|---|---|
| No conditions and no nullity → gate exits non-zero, names the missing field | scope_conditions fails on ClaimState absent, detail naming ## Scope Conditions, remedy naming the nullity token |
TestReadyScopeConditionsAbsent (core), TestIntentReadyMissingConditionsExit1 (CLI) |
| Edited condition text → identity unchanged | The marker is bytes inside the bullet; ParseClaims reads it positionally and Plan stamps only unmarked bullets |
TestConditionIdentitySurvivesEdit |
## Mechanism carries the nullity token → gate passes, reports the recorded nullity |
mechanism_claim returns OK with detail "mechanism claim declined (nullity recorded)" on ClaimState nullity |
TestReadyMechanismNullityPasses |
## Mechanism present but empty → gate exits non-zero, names the section |
ClaimState empty is a distinct state and fails with the write-the-claim-or-the-token remedy |
TestReadyMechanismEmptyFails |
| Duplicated or missing marker → gate exits non-zero, names the fault | scope_conditions reports each offending bullet by its ordinal and, for a duplicate, by the repeated cond- id |
TestReadyConditionMarkerMissing, TestReadyConditionMarkerDuplicated |
Tests
Every case below is written to fail against the current tree first: ParseClaims
does not exist, ReadyResult.Checks is asserted at length four in
ready_test.go, and Plan writes no markers.
internal/core/intent/claims_test.go:TestParseClaimsThreeByteStates(absent, empty, nullity, stated for both sections),TestNullityTokenIsExact(a "none stated" line without the full stop, or with trailing prose, isstated),TestParseConditionsMarkerExtraction,TestConditionIdentitySurvivesEdit,TestConditionMarkerSurvivesAReflow,TestConditionWithTwoMarkersIsAFault; the stamp's own cases (TestStampScopeConditionsMarksOnlyUnmarkedBullets— the split case,TestStampScopeConditionsIsIdempotent,TestStampScopeConditionsRedrawsOnCollision) andPlanend to end (TestPlanStampsConditionIdentities,TestPlanStampsAPlannedRecordInPlace,TestPlanOnAPlannedRecordWithNothingToStampRefuses,TestSeedDraftCarriesClaimSections). They live beside the code under test rather than in alifecycle_test.go/create_test.gosplit, matching where this package already keeps itsPlanand create cases.internal/core/intent/claims_fence_test.go: the fence, duplicate-heading, malformed-marker and mint-lock cases, plusTestFenceAwareBoundLeavesEveryAuditReceiptUnchanged, which pins every section body in the real corpus byte-identical across the fence-aware bound — the acceptance-criteria body's sha256 is a parked review receipt.internal/core/intent/ready_test.go: The five gate cases in the table, plusTestReadyChecksOrderAndCount(six checks, fixed order),TestReadyDisciplineExemptFromClaimChecks,TestReadyClaimChecksNotApplicableInTerminalBuckets,TestReadyScaffoldPromptIsNotAClaimandTestReadyReportsStructuralConditionFaults.internal/surface/cli/intent_cli_test.go:TestIntentReadyJSONRendersConditionIdentities(the identity's observable surface),TestIntentPlanStampsAPlannedRecord(the remedy the gate names runs on the record that printed it) and the exit-code cases for the two new refusals.
Out of scope
- The gradient's rationale, its staging argument and its exemptions: the discipline record itd-190 owns them, and this spec implements rather than restates them.
- Scope-condition dispositions: spc-59 populates them at verdict ingest. spc-55 only makes them attachable.
- Any auditor-side flag for a mechanism-nullity intent: the gradient defers it to the record that owns verdicts.
- Retro-fitting claims onto
shipped/orsuperseded/records. Population is forward-only, and an absent stamp is never backfilled.
Frontmatter
| id | spc-55 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-55-an-intent-s-claims-are-typed-and-its.md
open on GitHub ↗ · commit history ↗
References1
- itd-177An intent's claims are typed and its scope conditions keep their identity across edits — the readiness gate refuses an intent that leaves a context claim unrecordedimplements
mentions14
- adr-45Record ids are timestamp-numeric, capture-stable, and never allocated by looking at the maximum
- adr-51An intent can declare its mechanism claim and its scope conditions
- 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-2608300210588414intent ready reports a scope_conditions failure with a backfill remedy on shipped and superseded intents (18 shipped records), which spc-55's Out of scope forbids; and an untouched Mechanism scaffold line is reported as a stated mechanism claim, contradicting the plugin surface's line that the scaffold is not a claim. Detail strings on checks whose verdicts are already settled by the bucket check, so misleading output rather than a wrong gate.
- iss-2608300210588874A planned intent whose scope-condition bullet carries no identity marker is permanently NOT READY: the scope_conditions remedy names abcd intent plan, which exits 2 on a planned record, and the plugin surface forbids hand-typing markers. This is the default path (the scaffold leaves prose, plan stamps nothing, the maintainer writes the bullet after planning) and lands on every corpus record the moment a real condition replaces the nullity token. spc-55 states that Plan is idempotent and a re-run stamps only the unmarked bullets; the code refuses the re-run instead.
- iss-2608300235377731A scope-condition identity marker is recognised only when it closes the bullet's first physical line, so an ordinary editor reflow of an 80-column bullet (or prose appended after the marker) moves the marker onto a continuation line, the gate reports the condition unmarked, the named remedy mints a new id, and the old id is folded into the condition text — orphaning any disposition keyed on it. This violates the identity-survives-edits criterion and never-renumbered. Match a well-formed marker anywhere in the bullet including folded continuation lines, strip it from the text, fault on two markers in one bullet, and stamp only marker-free bullets.
- iss-2608300235386715itd-177 review residue: spc-55 names tests in lifecycle_test.go that do not exist (the equivalents live in claims_test.go under other names) and claims the gate reports a deletion as a marker known to the record's history, which no code holds; the stamp-only --json emits an empty spec object while the intent carries a spec_id; the two fix commits moved their issue records to resolved without a Resolves trailer (legal, but the trailer is the convention when the fixing change is the resolving one).
- 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-181A shipped intent's scope conditions are dispositioned by the fidelity verdict — what was assumed ex ante and what survived are recorded as different things
- itd-190The claim recording gradient: an intent's three claim kinds carry three recording requirements, and the readiness gate holds them
- itd-2609020625400445A committed preset declares the window it was calibrated for, and the record holds every preset to its declaration
- spc-59Scope-condition disposition at verdict ingest
- spc-2609020626046252A second writer into the disposition surface: `abcd intent condition` dispositions one scope condition from a reading item or a delivered intent
- 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