Documentation
¶
Overview ¶
Package recordid is abcd's canonical home for the record-id space: the native timestamp-numeric mint (mint.go), the read-side resolver (resolve.go), and the one record-filename grammar both sides parse (this file).
THE MINT (adr-45; mechanics per spc-33): every minting family allocates <family>-<yymmddHHMMSS><4 random digits> through Minter.Mint — time-ordered, coordination-free, offline, and reading no maximum anywhere, so two minters sharing a stale view, or two current checkouts sharing the same view, can never converge on one id. Captures (iss), intents (itd), specs (spc), the reading families and the scope-condition markers all mint this way; a family adopts the seam by holding a Minter and naming its family tag, never by carrying an allocator of its own (adr-45 ruling 3). Decisions (adr) mint here too, per the 2026-09-01 ruling adr-45 ruling 3 deferred: an ADR filed from then on is `adr-<stamp>` in a `<stamp>-<slug>.md` file, while the hand-numbered ordinals `0001`–`0058` keep their ids and their filenames. Both vintages are the same `[0-9]+` grammar, and CanonADRID / ADRFileID (resolve.go) are the one derivation every reader of either takes.
The armed record-lint uniqueness rules (issue_id_unique, intent_lifecycle, spec_id_unique) stay as the scheme's fail-safe — the cheap assertion that the scheme held — never the primary defence (adr-45 ruling 5).
Index ¶
- Constants
- Variables
- func ADRFileID(name string) string
- func ADRFiles(repoRoot string) ([]string, error)
- func BareFilenameNumRe(prefix string) *regexp.Regexp
- func CanonADRID(id string) string
- func CanonCitedID(s string) string
- func FilenameNumRe(prefix string) *regexp.Regexp
- func HandleInText(s string) (string, bool)
- func LookupOne(repoRoot, id string) (string, bool, error)
- func SameID(a, b string) bool
- func Slug(text string, max int) string
- func SplitRecordFilename(family, name string) (id, slug string, ok bool)
- func ValidAdmissionID(id string) bool
- func ValidIntentID(id string) bool
- func ValidReadingItemID(id string) bool
- func ValidReadingRunID(id string) bool
- func ValidReframeID(id string) bool
- func ValidSpecID(id string) bool
- func ValidSurpriseID(id string) bool
- type AmbiguousIDError
- type Minter
- type Resolver
Constants ¶
const ADRsRelDir = ".abcd/development/decisions/adrs"
ADRsRelDir is the decision store's root under a repository, repo-relative and slash-separated, spelled once for the resolver and every reader that walks the store the resolver's way.
const IssuesRelDir = ".abcd/work/issues"
IssuesRelDir is the issue ledger's root under a repository, repo-relative and slash-separated. It is the one spelling: every package that reaches the ledger names it through this constant, so a move of the ledger is one edit.
Variables ¶
var CitedIDRe = regexp.MustCompile(`^(?:adr|itd|iss|spc)-[0-9]+$`)
CitedIDRe is the canonical grammar of a cited record id: one of the four id-bearing family prefixes, a hyphen, and a bare decimal N. It is exported because every ingest boundary that accepts a citation must bound and shape the string BEFORE it is looked up or echoed, and they must all agree on the shape.
Functions ¶
func ADRFileID ¶ added in v0.8.0
ADRFileID derives an ADR's canonical id from its filename: `0037-<slug>.md` → `adr-37`, `2609012206053814-<slug>.md` → `adr-2609012206053814`. "" when the name is not an ADR record's.
It is exported because two sides must judge exactly the same files: the read-side resolver below, and the mint's presence check (core/decide), which redraws when a candidate id already names a record. A second copy of this derivation would let the mint re-issue an id the resolver already answers for.
func ADRFiles ¶ added in v0.12.0
ADRFiles lists every file in the decision store that carries an ADR id, repo-relative and sorted, read exactly as the resolver reads the store: the root and one bucket level, never through a link. It is the one listing record-lint's adr_id_unique judges, so the gate and the lookup it protects cannot disagree about which files claim a decision.
func BareFilenameNumRe ¶ added in v0.11.1
BareFilenameNumRe is the filename grammar of a family whose readers open a record by its bare handle alone, <prefix>-<N>.md, capturing N: the reading items and dispositions, which the outstanding report, the item locator and capture's disposition walk each find as `<handle>.md` and nothing else. A gate holding such a family to FilenameNumRe passed a `<prefix>-<N>-<slug>.md` no reader ever opens (iss-2608300929274006).
func CanonADRID ¶ added in v0.8.0
CanonADRID canonicalises an ADR id to the one spelling every claimant of that decision resolves to: lower-case `adr-` and the number with its leading zeros trimmed. "" when the string is not an ADR id at all.
ADRs are the one family carrying TWO id vintages: the hand-numbered ordinals 0001–0058, which keep their ids and filenames, and the minted `adr-<yymmddHHMMSS><rrrr>` form the 2026-09-01 ruling adopted. Both are the same `[0-9]+` grammar, so one canonicaliser serves both — which is the point of keeping it here rather than once per reader.
func CanonCitedID ¶ added in v0.9.0
CanonCitedID folds a cited id into the one spelling Lookup keys on: lower-case family, number with its leading zeros trimmed. "" when the string is not a cited id at all.
A record's PROSE writes a handle in whatever spelling reads best in the sentence — `ADR-6's concern`, `spc-009`, `Adr-0035` — while the resolver's keys are built from filenames and are uniformly lower-case and unpadded. Without one canonicaliser between them, a reader of the resolver would report a record that plainly exists as naming nothing, which is the single worst failure a citation gate can have: it trains the author to distrust it.
This is the general form of CanonADRID, which stays as the ADR-only door its two callers (the read-side resolver, the mint's presence check) already use. Both trim TEXTUALLY, never through an integer parse, for the reason canonADRNum states: a number wider than any integer type must still canonicalise rather than collapse to "not a record". An all-zero number is refused on the same terms — the allocator issues no zero id, so nothing can ever answer to one.
func FilenameNumRe ¶ added in v0.6.8
FilenameNumRe is the canonical record-filename grammar for a prose-handle family (iss/itd/spc): <prefix>-<N>[-<slug>].md, capturing N. It is the ONE grammar the read-side resolver matches, so a second copy in another package would drift and let a gate accept a filename the resolver later refuses when the record is cited. It is exported so record-lint validates each store's filenames against exactly this pattern rather than a looser local copy that accepted an arbitrary tail (iss-2608270908346617).
func HandleInText ¶ added in v0.11.1
HandleInText returns the first record handle the text carries, and whether it carries one. It is the one answer the principles lint and the reading assembler give to "does this statement cite?" (spc-2609020626042471): the lint refuses a typed principle whose statement cites, and the assembler refuses an assembly whose projected principle still does, so the two cannot disagree about what a citation looks like.
func LookupOne ¶ added in v0.11.0
LookupOne resolves one id against its own family's store alone, for a caller that needs a single record rather than a snapshot of the whole record: it reads one family where NewResolver reads four, so a fault in another family's store is not this lookup's refusal. An id whose prefix names no family resolves to nothing, as Lookup would answer it; an id two files claim is Lookup's *AmbiguousIDError.
func SameID ¶ added in v0.9.0
SameID reports whether two references name the same record — CanonCitedID's comparison, and the one every reader of a record link has to make.
It exists because the record writes one handle in more than one spelling. A spec's `intent:` back-link, an intent's `spec_id`, a citation in prose: all three are `itd-7`, `itd-007` or `ITD-7` at the author's discretion, and record-lint calls every spelling green. A reader comparing the strings literally therefore disagrees with the gate about two records that plainly match — and where the readers of ONE question disagree with each other, the question has two answers: an intent can ship while a spec still holds it open, and that spec becomes unclosable because the verb cannot find the intent the lint can. One primitive is what stops that, so every intent-link comparison goes through here.
A value that is not a cited id at all matches NOTHING, including another such value: `intent: null` on two specs does not make them realise one pseudo-record named "null". That is the fail-closed half — an unresolvable link is a defect for the lint to report, never a group to join.
func Slug ¶ added in v0.10.0
Slug derives a record filename's slug from free text: lowercased, split into words on every non-alphanumeric run, each word's hyphen runs collapsed and its ends trimmed, the words joined by hyphens, and the length capped at max. The cap lands between words — as many whole words as fit — so a slug never ends in a token the text did not contain: a hard cut turned "…criterion ac-10" into "…-ac-1", which reads as a record about a different criterion. Only a first word that alone exceeds the cap is cut inside, since there is no boundary to land on. The result may be empty when the text has no slug-able characters; callers refuse that case in their own words.
func SplitRecordFilename ¶ added in v0.6.8
SplitRecordFilename splits a record filename of the given family into the id it claims and the slug segment that follows it. family is the prefix without its hyphen ("iss", "itd", "spc") or "" for the ADR store's zero-padded numeric names; a filename of any other family does not match, so a stray record in a store's directory is not read as one of that store's.
It is the ONE splitter for that question, called by both the ledger reader (core/capture's filename ↔ frontmatter invariants) and the record-lint gate that judges the committed corpus — the same reason ValidIntentID and ValidSpecID live here rather than being restated on each side. Two hand-kept copies would let a record pass the gate and then fail the reader, which is the split this package exists to prevent.
The slug it returns is compared EXACTLY, never as a prefix. Every store applies its length cap while deriving the slug — before that one value forks into the filename and the frontmatter — so a filename is never a truncated form of a longer field, and a prefix tolerance would license the drift the comparison is there to catch.
func ValidAdmissionID ¶ added in v0.11.1
ValidAdmissionID reports whether id is a well-formed admission id (adm-N).
The admission verb builds `admissions/<run>/adm-N.md` out of it and the record dispatcher walks to that file by it, so it joins the grammars above for their reason: no path is built from an id nothing has matched (spc-2609020626040342).
func ValidIntentID ¶ added in v0.6.7
ValidIntentID reports whether id is a well-formed intent id (itd-N).
func ValidReadingItemID ¶ added in v0.7.0
ValidReadingItemID reports whether id is a well-formed reading item id (rdi-N).
It joins its run-id sibling for the same reason. The ledger writer builds a record filename out of it, and the cold-reading ingest verb's rollback DELETES by it — a delete bounded by a pattern is only as bounded as the pattern, and two copies of one grammar is how the writer and the deleter come to disagree about which files belong to a run.
func ValidReadingRunID ¶ added in v0.7.0
ValidReadingRunID reports whether id is a well-formed reading run id (rdg-N).
It is the THIRD path-building record id, and it joins the two above for their reason rather than getting a fourth copy of the pattern: the reading ledger writer (core/capture) and the cold-reading ingest verb (core/reading) both build a directory name out of a run id that arrives from outside — an ingest's run id comes off an untrusted payload — so a traversal id must be refused by exactly one rule, before either side touches a path.
func ValidReframeID ¶ added in v0.11.1
ValidReframeID reports whether id is a well-formed reframe id (rfm-N), on the same terms: the reframe verb writes `reframes/rfm-N.md`, completes it by id, and the dispatcher reads it back (spc-2609020626048705).
func ValidSpecID ¶ added in v0.6.7
ValidSpecID reports whether id is a well-formed spec id (spc-N).
func ValidSurpriseID ¶ added in v0.11.1
ValidSurpriseID reports whether id is a well-formed surprise id (srp-N), on the same terms as ValidAdmissionID: the surprise verb writes `surprises/srp-N.md` and the dispatcher reads it back.
Types ¶
type AmbiguousIDError ¶ added in v0.12.0
type AmbiguousIDError struct {
ID string
Paths []string // repo-relative, slash-separated, in scan order
}
AmbiguousIDError is the refusal a lookup gives when two or more files in one family answer to the same id. Every reader that resolves an id to a file wants ONE file; answering with whichever sorts first would let a second claimant (an `0037-a.md` beside `0037-x.md`) speak for the record, so the lookup names every claimant and lets record-lint's uniqueness rules say which one has to go.
func (*AmbiguousIDError) Error ¶ added in v0.12.0
func (e *AmbiguousIDError) Error() string
type Minter ¶
type Minter struct {
// Now supplies the mint instant; nil means time.Now.
Now func() time.Time
// Entropy supplies the suffix draw; nil means crypto/rand.Reader.
Entropy io.Reader
}
Minter mints native timestamp-numeric record ids. Both fields are seams for tests; the zero value is the production configuration (time.Now and crypto/rand).
func (Minter) Mint ¶
Mint returns a fresh native id for family (e.g. "iss"): the family tag, the UTC second stamp, and a uniform 4-digit suffix. It reads nothing — no ledger, no refs, no maximum — so two minters can never converge on one id by sharing a stale view; only a same-second suffix coincidence remains, which is the armed uniqueness detectors' residue to assert (adr-45 ruling 5).
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver is the live record-id set of one repository, read once. It is a snapshot: build it at the start of a validation pass and use it throughout, so every citation in one payload is judged against the same view of the record.
func NewResolver ¶
NewResolver reads repoRoot's record families and returns the resolver over them.
An ABSENT family is not an error — a repository need not carry every store, and its ids simply do not resolve. An unreadable family that IS present IS an error: reporting "this id names no record" when the truth is "the store could not be read" would refuse a correct citation, so the scan fails closed and lets the caller say what actually happened.
func (*Resolver) Has ¶ added in v0.12.0
Has reports whether any file answers to the record id. It is the existence question alone — does a citation name a real record — which a duplicate does not change: the id names a record either way, and the duplicate is record-lint's uniqueness finding, not a missing citation.
func (*Resolver) Lookup ¶
Lookup returns the repo-relative path of the record id, and whether it exists. A malformed id simply does not resolve — callers that must distinguish "malformed" from "absent" check CitedIDRe first, which they do anyway to bound the string before echoing it. An id two or more files claim is refused with an *AmbiguousIDError naming them all: the caller asked which file IS the record, and no answer but "these, and the record must be repaired" is true.