Documentation
¶
Overview ¶
Package intent is abcd's transport-agnostic native intent store (intent lifecycle, itd-80). It owns the in-memory model of intent records and the disk operations that plan, link, and summarise them. Every function takes a structured request and returns a structured result; nothing here writes to stdout or knows about a CLI, MCP, or hook surface — the front doors under internal/surface/* marshal these results for their transport.
An intent record is a markdown file under <repoRoot>/.abcd/development/intents/{drafts,planned,shipped,disciplines, superseded}/itd-N-<slug>.md. The bucket directory IS the lifecycle state (directory-as-truth: there is no status: frontmatter field), mirroring the native spec store. The load-bearing field is spec_id: spc-N, the intent's derived side of the bidirectional link to the spec that realises it (the spec's reciprocal side is intent: itd-N).
Frontmatter is read by the shared internal/core/frontmatter line scanner, not a YAML parser — the package pulls in zero new dependencies. Ids are validated against strict regexes before any path is built, so a hostile id can never traverse out of the intent store.
Index ¶
- Constants
- Variables
- func DuplicateConditionIDs(conds []ScopeCondition) []string
- func IsClaimPrompt(body string) bool
- func IsSeedNote(text string) bool
- func MalformedMarkerOrdinals(conds []ScopeCondition) []int
- func MultiplyMarkedConditions(conds []ScopeCondition) []int
- func ParseGrounds(content string) []grounds.Grounds
- func UnmarkedConditionOrdinals(conds []ScopeCondition) []int
- func Validate(it Intent) error
- type AuditEmitResult
- type ClaimState
- type Claims
- type Corpus
- type DraftOptions
- type GroundsResult
- type IngestVerdictResult
- type Intent
- type LinkResult
- type LinkedPair
- type PlanResult
- type ReadyCheck
- type ReadyResult
- type ReconcileResult
- type ScopeCondition
- type StatusView
Constants ¶
const ( MechanismPrompt = "" /* 293-byte string literal not displayed */ ScopeConditionsPrompt = "" /* 300-byte string literal not displayed */ )
The two claim-section prompts this package seeds. They are the contract a human replaces, not a claim a human made, so a gate that reads one has to be able to say which it is looking at.
const ( BucketDrafts = "drafts" BucketPlanned = "planned" BucketShipped = "shipped" BucketDisciplines = "disciplines" BucketSuperseded = "superseded" )
Lifecycle buckets. The directory an intent file lives in is its state.
const ( CheckBucket = "bucket" CheckAcceptanceCriteria = "acceptance_criteria" CheckMechanismClaim = "mechanism_claim" CheckScopeConditions = "scope_conditions" CheckSpecLink = "spec_link" CheckSpecBody = "spec_body" CheckGrounds = "grounds" )
Check names, in the fixed order Ready always reports them.
const CaptureSeedNote = captureSeedOpening + " " + seedNoteTail
CaptureSeedNote is the placeholder a quoted-text capture mints, whole.
const GroundsHeading = grounds.Heading
GroundsHeading is the section an intent record carries its grounds under — core/grounds's one spelling, so the intent half and the ledger half name the same section.
const IntentsRelDir = ".abcd/development/intents"
IntentsRelDir is the intent-store root, relative to the repo worktree.
const KindStandalone = "standalone"
KindStandalone is the default binding kind Plan writes (a 1:1 intent↔spec).
const NullityToken = "None stated."
NullityToken is the one exact spelling that records a claim as considered and declined: alone on its line under the section heading, byte for byte. Prose that merely contains the word "none" is a stated claim — the whole point of an exact token is that a gate can tell a declined claim from a discussed one.
const VerdictType = "abcd/intent-fidelity-verdict/v1"
VerdictType is the only _type the ingest accepts.
Variables ¶
var Buckets = []string{BucketDrafts, BucketPlanned, BucketShipped, BucketDisciplines, BucketSuperseded}
Buckets is the fixed lifecycle order used for loading and rendering.
Functions ¶
func DuplicateConditionIDs ¶ added in v0.7.0
func DuplicateConditionIDs(conds []ScopeCondition) []string
DuplicateConditionIDs returns the identity markers carried by more than one condition, in first-seen order. Two conditions sharing an identity is a fault the gate names rather than repairs: a disposition keyed on a duplicated marker would attach to whichever bullet the reader happened to reach first.
func IsClaimPrompt ¶ added in v0.7.0
IsClaimPrompt reports whether a claim section's body is still one of the prompts above, rather than something somebody wrote.
It lives HERE, with the templates, for the reason IsSeedNote does: a consumer comparing against its own copy of the sentence would keep matching the old wording the moment these are reworded, and the symptom would be an unanswered prompt reported to a reader as a recorded claim.
func IsSeedNote ¶
IsSeedNote reports whether a press-release body is still one of the templates this package writes, rather than something somebody wrote.
It lives HERE, with the templates, because it is the only place that can stay true: a consumer comparing against its own copy of the sentence would keep matching the old wording the moment these are reworded, and the symptom would be a placeholder quoted at a reader as a testimonial. Both minted forms are covered and nothing else is — the promotion form's source id is the only part allowed to vary, and a body with a single written sentence in it fails.
The caller passes the body already reduced to its words: quote markers, emphasis markers and whitespace runs removed.
func MalformedMarkerOrdinals ¶ added in v0.7.0
func MalformedMarkerOrdinals(conds []ScopeCondition) []int
MalformedMarkerOrdinals returns the 1-based positions of bullets carrying a near-miss of an identity marker.
func MultiplyMarkedConditions ¶ added in v0.7.0
func MultiplyMarkedConditions(conds []ScopeCondition) []int
MultiplyMarkedConditions returns the 1-based positions of bullets carrying more than one identity.
func ParseGrounds ¶ added in v0.7.0
ParseGrounds reads an intent record's recorded grounds, in the order they were written — core/grounds's section reader, held to the SUBSTANCE FLOOR.
The floor applies on this side and not on the ledger's: the value it judges was written by this package's own writer, which validated it, whereas a wontfix stamps its grounds from a reason whose contract is merely non-empty. The readiness gate CLAIMS the floor, so a reader that did not apply it would let `- pursued: yes` satisfy a check that was then enforcing only a colon (iss-2608300930057882).
It reads the record BODY, which is the scope the writer appends into. Asked of the whole file it would match a frontmatter `# Grounds` comment — a legal YAML comment the block parser skips and an ATX heading pattern matches — as the section, and report an empty pseudo-section about a record whose body carries its entries (iss-2608301805069999). Callers pass whole records and bodies alike; grounds.Body takes either.
func UnmarkedConditionOrdinals ¶ added in v0.7.0
func UnmarkedConditionOrdinals(conds []ScopeCondition) []int
UnmarkedConditionOrdinals returns the 1-based positions of the conditions no `Plan` run has stamped yet.
Types ¶
type AuditEmitResult ¶
type AuditEmitResult struct {
ReceiptID string `json:"receipt_id"`
IntentID string `json:"intent_id"`
Status string `json:"status"` // owed | already_owed | already_ingested | already_dead_letter
RequestPath string `json:"request_path"`
}
AuditEmitResult reports one emit (OWED stub + request file).
func ReEmitAudit ¶
func ReEmitAudit(repoRoot, intentID string) (AuditEmitResult, error)
ReEmitAudit handles the manual `abcd intent audit <itd-N>` verb for a shipped intent. It resolves the intent, refuses one not in shipped/, and delegates to emitAuditForIntent. Behaviour depends on the intent's current review state: an OWED receipt (or none) (re-)parks the OWED stub and rewrites its ephemeral request; a TERMINAL receipt is not re-reviewed — an already-INGESTED or already-DEAD_LETTER receipt returns that status unchanged (re-reviewing would discard the recorded audit), so the caller learns the review is already resolved rather than silently receiving a fresh stub.
type ClaimState ¶ added in v0.7.0
type ClaimState string
ClaimState is the byte state of a claim section — the three states the gradient refuses to collapse, plus the ordinary one. An ABSENT section is a claim not carried; an EMPTY section is a gate fault; the NULLITY token is a claim considered and declined.
const ( ClaimAbsent ClaimState = "absent" ClaimEmpty ClaimState = "empty" ClaimNullity ClaimState = "nullity" ClaimStated ClaimState = "stated" )
type Claims ¶ added in v0.7.0
type Claims struct {
Mechanism ClaimState `json:"mechanism"`
MechanismPrompt bool `json:"mechanism_prompt"`
ConditionsState ClaimState `json:"conditions_state"`
ConditionsPrompt bool `json:"conditions_prompt"`
Conditions []ScopeCondition `json:"conditions"` // populated only when stated
// ConditionsFenced and ConditionsDuplicated are structural faults in the
// `## Scope Conditions` section that make its bullets unsafe to WRITE: a
// fenced example a stamp could land inside, and a second heading that makes
// "the section" ambiguous. Both are reported by the gate and refused by the
// stamp, so the remedy the gate names is never a command that cannot run.
ConditionsFenced bool `json:"conditions_fenced,omitempty"`
ConditionsCommented bool `json:"conditions_commented,omitempty"`
ConditionsDuplicated bool `json:"conditions_duplicated,omitempty"`
}
Claims is what one intent record says about its mechanism and context claims.
The two Prompt flags mark a section still holding the create-path scaffold: bytes that ASK for a claim. They read as ClaimStated because that is what they are on disk, and the flag is what stops a gate reporting an unanswered question as an answer (iss-2608300210588414).
func ParseClaims ¶ added in v0.7.0
ParseClaims reads an intent record's `## Mechanism` and `## Scope Conditions` sections. It is the single reader for both: every downstream consumer asks it rather than re-deciding what a section, a bullet, or the nullity token is.
type Corpus ¶
type Corpus struct {
Intents []Intent `json:"intents"`
}
Corpus is the in-memory set of intent records discovered across every bucket.
func Load ¶
Load discovers intent files across every lifecycle bucket, parses their frontmatter, and returns the in-memory Corpus. A missing intents/ directory (or a missing individual bucket) yields no records for it (soft, mirroring spec.Load). A present-but-malformed intent file — one whose frontmatter lacks a well-formed id — is a hard, loud error.
type DraftOptions ¶
type DraftOptions struct {
Slug string
Title string
SeedBody string
Impact string
PromotedFrom string
// Origin is the draft's arrival path (itd-178). It is DERIVED from which
// command ran, never carried as free text: the empty value means the default
// (a verb a person invoked), and capture.Promote — the one shipped path that
// derives a record from another record — passes extracted-from-record.
Origin provenance.Kind
// ProductionMode is how the seed text was produced. Empty takes
// provenance.DefaultMode, so a draft written through a command carries the key
// whatever the caller says.
ProductionMode string
}
DraftOptions parameterises CreateDraft: the explicit slug and title, the Why This Matters seed body, an optional impact judgement, and — on the promote path (spc-24) — the iss-N the draft graduated from, written as the promoted_from back-edge (an iss-N, or the rdi-N of a dispositioned reading item).
type GroundsResult ¶ added in v0.7.0
type GroundsResult struct {
IntentID string `json:"intent_id"`
Path string `json:"path"` // repo-relative intent path
Token string `json:"token"` // pursued | deferred | declined
Text string `json:"text"` // the text as WRITTEN (post-redaction)
Entries int `json:"entries"`
Redacted int `json:"redacted,omitempty"`
}
GroundsResult is the outcome of one RecordGrounds call. Redacted counts the spans the redactor rewrote before the write, so a surface can SAY the text was altered: rewriting somebody's reasoning in silence is worse than not recording it.
func RecordGrounds ¶ added in v0.7.0
func RecordGrounds(repoRoot, intentID string, g grounds.Grounds) (GroundsResult, error)
RecordGrounds appends one grounds entry to an intent record, creating the `## Grounds` section when it is absent.
Recording is APPEND-ONLY: a second gate decision adds an entry beside the first rather than replacing it, because the earlier conjecture is precisely what a later reader checks the outcome against. Rewriting it would leave the record saying only what was believed last.
The text is redacted BEFORE it is validated, never after, so no rewritten span can reach a field the validator has already passed; the redactor is the same fail-closed one the quoted-text create path uses, because a grounds text is durable committed prose. The write goes through writeIntentFile, the package's one intent-record writer.
type IngestVerdictResult ¶
type IngestVerdictResult struct {
Status string `json:"status"` // ingested | dead_letter | noop
ReceiptID string `json:"receipt_id"`
IntentID string `json:"intent_id"`
Criteria int `json:"criteria"`
Met int `json:"met"`
MetWithConcern int `json:"met_with_concerns"`
NotMet int `json:"not_met"`
Inconclusive int `json:"inconclusive"`
Conditions int `json:"conditions"`
Survived int `json:"survived"`
Narrowed int `json:"narrowed"`
Falsified int `json:"falsified"`
Untested int `json:"untested"`
DeadLetterPath string `json:"dead_letter_path,omitempty"`
Reason string `json:"reason,omitempty"`
}
IngestVerdictResult reports one verdict ingest.
func IngestVerdict ¶
func IngestVerdict(repoRoot, verdictPath string) (IngestVerdictResult, error)
IngestVerdict reads an untrusted intent-fidelity verdict JSON and applies it to the committed record, FAIL-CLOSED at every step:
- malformed/oversize/unreadable payload with no resolvable receipt -> reject;
- receipt matching no parked marker (unsolicited) -> reject;
- already INGESTED for this receipt -> no-op;
- schema/semantic validation failure on a resolvable receipt -> DEAD_LETTER (marker + INCONCLUSIVE criteria + retained raw payload), never partial;
- otherwise -> INGESTED (OWED stub replaced by the rendered verdict).
type Intent ¶
type Intent struct {
ID string `json:"id"` // itd-N
Slug string `json:"slug"` // kebab-case
Kind string `json:"kind"` // standalone | bundle-member | discipline | null
SpecID string `json:"spec_id"` // spc-N, the derived link (may be null)
Bucket string `json:"bucket"` // lifecycle directory (directory-as-truth)
Path string `json:"path"` // repo-relative markdown path
// PromotedFrom is the iss-N this intent graduated from (spc-24's two-sided
// promote edge). Parsed leniently: absent on every non-promoted record.
PromotedFrom string `json:"promoted_from,omitempty"`
}
Intent is one intent record. Bucket is the directory it was found in; Path is repo-relative (never an absolute local path).
func CreateDraft ¶
func CreateDraft(repoRoot string, opts DraftOptions) (Intent, string, error)
CreateDraft is the one canonical draft-mint primitive: both the quoted-text create (CreateFromText) and the capture-promote path (capture.Promote, which supplies an explicit slug and seed body) route through it, so a draft can never be minted outside the store mint lock. It validates every option at the boundary — the slug becomes a filename — mints the next itd-N, and atomically writes drafts/itd-N-<slug>.md. On any refusal nothing is written.
func CreateFromText ¶
CreateFromText files a new draft intent seeded from free-form text, mirroring the capture engine's create shape: it derives a filename-safe slug, mints the next itd-N under the exclusive store mint lock (so two concurrent sessions never mint the same id), and atomically writes drafts/itd-N-<slug>.md with the canonical draft frontmatter set and a minimal, honest body skeleton carrying the text. Empty/whitespace text is refused and nothing is written.
The seeded record is lint-valid (intent_lifecycle accepts a draft whose kind is null and whose spec_id is null) and passes Validate; a human expands it, then `abcd intent plan` schedules it. This is the quoted-text create path itd-46 delivers — the create half of what spc-6 AC3 (promote) needs.
type LinkResult ¶
LinkResult reports a completed Link: the updated intent and the spec it now declares.
func Link ¶
func Link(repoRoot, intentID, specID string) (LinkResult, error)
Link retroactively writes the derived spec_id link on an existing planned intent for an existing spec. It validates both ids, that the intent is in planned/, and that the spec exists AND already declares this intent (the reciprocal intent: itd-N side); a spec that realises a different intent is a mismatch and fails closed rather than forging a one-sided link.
type LinkedPair ¶
LinkedPair is one intent↔spec link in the lifecycle summary.
type PlanResult ¶
type PlanResult struct {
Intent Intent `json:"intent"`
Spec spec.Spec `json:"spec"`
MintWarning string `json:"mint_warning,omitempty"`
// ConditionsStamped is how many scope-condition bullets this run gave an
// identity to.
ConditionsStamped int `json:"conditions_stamped"`
// StampOnly reports that this run did the identity step alone, over a record
// already in planned/: no spec was minted and no bucket moved. It is how a
// condition written after planning reaches the mint, which is what makes the
// readiness gate's remedy a command that works.
StampOnly bool `json:"stamp_only"`
}
PlanResult reports a completed Plan: the updated planned intent and the spec minted to realise it. MintWarning is the loud-degrade note from the spec-id refs-union scan (empty when the scan completed) — the surface MUST render it so a degrade to working-tree-only minting is never silent.
func Plan ¶
func Plan(repoRoot, intentID, productionMode string) (PlanResult, error)
Plan is the load-bearing verb. For a draft intent carrying a non-empty `## Acceptance Criteria` section, it mints a native spec, writes the intent's derived side of the bidirectional link (spec_id + a default kind), and moves the intent drafts/ → planned/.
It is fail-closed: every intermediate on-disk state satisfies the intent_lifecycle record-lint rule, so a failure at any step leaves a consistent, lint-valid record rather than a half-written link. The order — create spec, set kind while still a draft, move to planned, then write spec_id — is chosen so that (kind=standalone, spec_id=null) is the only transient frontmatter, and that shape is valid in BOTH drafts and planned.
productionMode is the disclosure the MINTED SPEC carries (itd-178); it is validated by the spec store before the id is minted, and an empty value takes the vocabulary's default. It has no bearing on the intent record, whose own stamp was written when the draft was created and is never rewritten.
type ReadyCheck ¶
type ReadyCheck struct {
Name string `json:"name"` // bucket | acceptance_criteria | mechanism_claim | scope_conditions | spec_link | spec_body | grounds
OK bool `json:"ok"`
Detail string `json:"detail"` // why it passed or failed
Remedy string `json:"remedy,omitempty"` // the exact next command/action when !OK
}
ReadyCheck is one finding of the implement-readiness gate.
type ReadyResult ¶
type ReadyResult struct {
IntentID string `json:"intent_id"`
Path string `json:"path"` // repo-relative intent path
Bucket string `json:"bucket"` // directory-as-truth state
SpecID string `json:"spec_id"`
Ready bool `json:"ready"`
Checks []ReadyCheck `json:"checks"` // always exactly 7, fixed order
// Conditions is the record's scope conditions with their minted identities —
// the observable surface the identity criteria assert against. Empty for a
// record whose conditions are absent, or recorded as the nullity token.
Conditions []ScopeCondition `json:"conditions"`
}
ReadyResult is the structured readiness verdict for one intent: may this intent be implemented now? Every check is always evaluated and reported, so a surface presents the full picture rather than the first failure.
func Ready ¶
func Ready(repoRoot, intentID string) (ReadyResult, error)
Ready reports whether an intent is ready to implement: planned (directory-as-truth), carrying enumerable Acceptance Criteria, linked bidirectionally to a spec, and that spec's body written past its minted stub. It is a read-only reporter — the machine-checkable form of the run protocol's "is this item ready?" question — and never mutates the store.
"Not ready" is a result, never an error: error is reserved for structural faults (malformed id, unknown intent, unreadable record, store load failure), so a surface can map result vs error to distinct exit codes.
type ReconcileResult ¶
type ReconcileResult struct {
Spec spec.Spec `json:"spec"`
Intent Intent `json:"intent"`
IntentMoved bool `json:"intent_moved"`
From string `json:"from"`
To string `json:"to"`
// ReceiptID is the deterministic fidelity-review receipt parked in the
// shipped intent's Audit Notes (empty if the emit failed).
ReceiptID string `json:"receipt_id,omitempty"`
// AuditEmitError is a NON-FATAL report of a failed review emit. The review is
// report-only, so the intent still ships; the surface prints this loudly.
AuditEmitError string `json:"audit_emit_error,omitempty"`
}
ReconcileResult reports a completed Reconcile (the deterministic half of `abcd spec close`): the closed spec, the linked intent in its post-reconcile state, whether the intent moved this call (false on an idempotent re-run), and the intent's bucket transition (From → To).
func Reconcile ¶
func Reconcile(repoRoot, specID string) (ReconcileResult, error)
Reconcile is the deterministic half of `abcd spec close`: it advances the intent a spec realises, then closes the spec, so one command marks the spec done AND ships its linked intent.
Ordering is intent-first, spec-last, so a partial failure is recoverable by re-running: the intent moves planned/ → shipped/ before spec.Close runs, so a failure at the move leaves the spec OPEN (retry-safe), never a closed spec with a still-planned intent. It is idempotent: an already-shipped intent is not re-moved, and a re-run on an already-closed spec is a clean no-op/complete rather than an error.
It fails closed with NO partial move when: the spec has no/empty intent link; the named intent does not exist; the link is ambiguous (more than one spec realises the intent); the intent's spec_id disagrees with this spec (bidirectional drift); or the intent is in an unexpected bucket (e.g. still in drafts — it was never planned). Every id is validated against the ^spc-/^itd- regexes before any path is built. The intent's `## Audit Notes` are left untouched (the fidelity audit is a later phase; the intent ships with them empty).
type ScopeCondition ¶ added in v0.7.0
type ScopeCondition struct {
Ordinal int `json:"ordinal"` // 1-based position in the section
ID string `json:"id"` // cond-<16 digits>, empty when unmarked
Text string `json:"text"` // the bullet's prose, marker removed
// ExtraIDs holds every well-formed marker after the first. A bullet carrying
// two identities is an ambiguity the gate names: a disposition keyed on one of
// them would attach to whichever the reader reached first.
ExtraIDs []string `json:"extra_ids,omitempty"`
// MalformedMarker reports a `<!-- cond:` in the bullet that is not a
// well-formed identity. Left unread it is silent prose, so the stamp glues a
// real marker beside it and the bullet ends up carrying two things that look
// like identities to a human and one to the machine.
MalformedMarker bool `json:"malformed_marker,omitempty"`
}
ScopeCondition is one recorded context claim: the condition's prose and the minted identity that outlives a rewrite of it. ID is empty for a bullet no `Plan` run has stamped yet — an unstamped condition is a gate finding, never silently repaired at read time.
type StatusView ¶
type StatusView struct {
Buckets map[string]int `json:"buckets"`
SpecsOpen int `json:"specs_open"`
SpecsClosed int `json:"specs_closed"`
Linked []LinkedPair `json:"linked"`
}
StatusView is the read-only lifecycle summary: intent counts by bucket, spec counts by status, and the linked intent↔spec pairs.
func Status ¶
func Status(repoRoot string) (StatusView, error)
Status builds the read-only lifecycle summary: intent counts by bucket, spec counts by status, and the intent↔spec links (every intent whose spec_id is non-null). Linked pairs are ordered by the corpus load order (bucket, then directory), which is deterministic.