Documentation
¶
Overview ¶
Package spec is abcd's transport-agnostic native spec store (intent lifecycle, itd-64). It owns the in-memory model of spec records and the disk operations that mint, load, and transition 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.
A spec record is a markdown file under <repoRoot>/.abcd/development/specs/{open,closed}/spc-N-<slug>.md. The bucket directory IS the status (directory-as-truth: open/ vs closed/, with no status: frontmatter field), mirroring how intents encode their lifecycle by bucket. The load-bearing field is intent: itd-N, the link a spec declares to the intent it realises.
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 spec store.
Index ¶
Constants ¶
const ( StatusOpen = "open" StatusClosed = "closed" )
Bucket directory names. The directory a spec file lives in is its status.
const ( // SpecsRelDir is the spec-store root, relative to the repo worktree. SpecsRelDir = ".abcd/development/specs" // IntentsRelDir is where intents live; NextID scans it for reserved spec ids. IntentsRelDir = ".abcd/development/intents" )
Repo-relative store locations.
Variables ¶
This section is empty.
Functions ¶
func BodyIsStub ¶
BodyIsStub reports whether a spec body still carries the minted placeholder — i.e. no human has written the design record yet.
func HasNum ¶ added in v0.6.7
HasNum reports whether v is a spec reference carrying a usable number. It is the tolerant counterpart of the ^spc-[0-9]+$ argument grammar, for validating a STORED spec_id, which the record lint lets carry a slug or zero padding.
func NextID ¶
NextID mints the next spec id. The rule is:
max(N over existing spec-store files ∪ N over every intent's spec_id frontmatter across .abcd/development/intents/** ∪ N over spec-store filenames on every other git ref) + 1
Scanning the intents is what keeps a freshly minted spec from colliding with a reservation: itd-3 shipped with spec_id: spc-1 but has no spec-store file, so a spec-only scan would hand out spc-1 again. Folding intent reservations in means the first minted id is spc-2 while that reservation stands.
Scanning every git ref (recordid.MaxAcrossRefs) is what keeps two parallel branches from re-minting the same spc-N: once branch A commits spc-10, branch B's ref scan sees it even though the file is absent from B's working tree (iss-115, iss-120). The returned mintWarning is non-empty (and MUST be surfaced) when that ref scan degraded to working-tree-only — git absent, not a repo, or a failed ref query — so the fallback is never silent. The cross-ref scan reads spec-store FILENAMES only; a spec_id reservation committed in an intent's frontmatter on another branch is not folded in (that narrower window is left to the record-lint uniqueness rules on the merged PR).
func SameNum ¶ added in v0.6.7
SameNum reports whether two spec references name the same spec number — the canonical comparison the record lint makes, so a verb using it can never refuse a spelling the lint accepts (spc-9, spc-9-widget, spc-009 are one spec). A value carrying no usable number (null, "spc-", "spc-abc", an over-int64 N) matches nothing, including another such value.
Types ¶
type Spec ¶
type Spec struct {
ID string `json:"id"` // spc-N
Slug string `json:"slug"` // kebab-case
Intent string `json:"intent"` // itd-N, the load-bearing link
Status string `json:"status"` // open | closed (directory-as-truth)
Path string `json:"path"` // repo-relative markdown path
}
Spec is one spec record. Status is the bucket it was found in; Path is repo-relative (never an absolute local path).
func Close ¶
Close moves a spec file open/ -> closed/ via os.Rename (atomic on one filesystem) and returns the updated Spec. It fails closed if the spec is missing or already closed. The linked intent is deliberately left untouched: moving it is a later reconcile concern that consumes Spec.Intent.
func Create ¶
Create mints an id via NextID and writes specs/open/spc-N-<slug>.md with the intent link and the origin/production_mode disclosure pair in frontmatter. Both the intent id and the slug are validated before any path is built (the slug becomes a filename), as is the production mode — a spec is minted by a verb a person invoked, so its arrival path is researcher-authored and is derived here rather than asked for. An empty mode takes the vocabulary's default. The write is atomic. The returned mintWarning is non-empty when the refs-union scan degraded to working-tree-only minting; the caller MUST surface it (never swallow it).
type Store ¶
type Store struct {
Specs []Spec `json:"specs"`
}
Store is the in-memory set of spec records discovered under both buckets.
func Load ¶
Load discovers spec files under both buckets, parses their frontmatter, and returns the in-memory Store. A missing specs/ directory yields an empty store (soft, mirroring lint's missing-dir behaviour). A present-but-malformed spec file is a hard, loud error.
func (Store) ByIntent ¶
ByIntent returns the spec linked to the given intent id; ok is false when no spec realises that intent.
func (Store) Lookup ¶
Lookup returns the spec the given reference names; ok is false when absent.
Matching is on the spec NUMBER, not the literal string, because that is the comparison record-lint makes: a spec_id is written bare (spc-9), with its slug (spc-9-widget), and zero-padded (spc-009) across the record, and all three are lint-green. A literal-only compare would let the lifecycle verbs refuse a record the lint accepts. An exact string match still wins when the store holds one, so a caller that names a record precisely gets that record; two specs sharing a number is a record defect the lint's spec_id_unique rule flags (the mint never produces one, since NextID allocates max+1 over the same numbers).