Development record
Record ids are timestamp-numeric, capture-stable, and never allocated by looking at the maximum
Context
Three live id collisions in two days (iss-330; the round-2 renumber; PR #386), one costing a full release re-cut, all by the same mechanism: max+1 allocation from a stale checkout, decided purely by merge timing. The graded field test confirmed the mechanism and that per-minter defensive protocols do not scale. Two adversarial reviews of itd-114 sized the alternative schemes against this repository's id grammar — a closed numeric type-system whose consumers fail silently on non-numeric ids — and the 2026-08-20 planning interview ruled. This record holds the trust rules that outlive the feature; the capability is itd-114's.
Decision
- The native mint is timestamp-numeric:
<family>-<yymmddHHMMSS><random digits>, time-ordered, coordination-free, offline, and matching the existing[0-9]+grammar so every consumer — the release bijection, the canonical resolver, the record-lint rules, list ordering, the id dispatch — holds unchanged. The random-digit width and same-instant tiebreak are the spec's to fix; the numeric, time-ordered format is not. - Ids are capture-stable: once minted, an id is never renumbered by any later process. (The renumbers of 2026-08-19/20 were the max+1 scheme's failure cost, not a precedent.) Legacy sequential ids stay exactly as minted, forever; the ledger is dual-vintage but single-grammar.
- Rollout is captures-first, then every family through the same seam: one allocator, per-family adoption as configuration, adr's zero-padded filename ordinal ruled at its own turn.
- The optional forge allocator allocates and never stores (itd-129's ledger-canonical line): when configured and offline, the mint falls back to the native scheme loudly — capture never blocks on network, the mint names the path it took, and the format stays uniform either way. This is the documented-fetch posture brief invariants 7 and 10 require.
- The uniqueness detectors stay as the cheap assertion that the scheme held — a fail-safe against scheme bugs, never again the primary defence.
Alternatives Considered
- ULID/UUIDv7-shaped ids: stronger entropy, but a non-numeric grammar breaks ~30 consumers — several silently (records dropping out of release cuts unreported, typed links unchecked, list order inverted) — and costs a permanent dual-grammar tax. Rejected on the ripple evidence.
- Forge-allocated numbers as the default: atomic and registry-visible, but covers one family, needs network at mint, and inverts the native-floor stance (brief invariant 2). Retained as the opt-in allocator only.
- Reserve-registry / random-suffix: a committed reservation file recreates merge contention on itself; random suffixes alone lose time order. Both rejected with reasons at the interview.
- Keep max+1 with per-minter protocols: the field test's P4 — the protocol saved the one minter carrying it and nobody else. Rejected.
Consequences
- itd-114 implements the mint; its spec fixes the entropy width and tiebreak.
- The corresponding brief invariant is invariant 11 in
02-constraints/03-invariants.md, landed with this record. issue_id_uniqueand its siblings remain armed, reinterpreted as scheme assertions.- The per-task defensive minting protocols retire once the scheme ships; until then they remain the documented interim.
Frontmatter
| id | adr-45 |
|---|---|
| type | adr |
| status | accepted |
| date | 2026-08-20 |
.abcd/development/decisions/adrs/0045-record-ids-are-timestamp-numeric-and.md
open on GitHub ↗ · commit history ↗
References3
- adr-44abcd never mutates a remote uninvited, and identity derives from caller-local factsrelated
- itd-114Two Agents Can Mint At The Same Instant And Never Colliderelated
- itd-129Forge mirror as an opt-in adapter: the ledger mirrors out to forge issues one way, the forge id lives in the record schema, closures without import self-heal, and the whole surface is loudly absent without forge authrelated
Referenced by4
- adr-2609151138420062The decisions log is a folder of minted records, not one appended filerelated
- adr-2609212115255771Phases and milestones are retired: sequencing is dependencies rendered as status, the checkpoint is the derived releaserelated
- adr-2610110925465593Decisions are one flat file each and the single log is retiredrelated
- adr-2610301638558260A published refusal code is never renamed or reusedrelated
mentions39
- dcn-26082000000000072026-08-20 — the spc-33 mint mechanics are ruled and the native mint ships
- dcn-26082100000000012026-08-21 — Bug-hunt round 4, branch bughunt-b/round-4. Baseline (make preflight
- dcn-26090100000000072026-09-01 — ADRs take timestamp ids through the same seam as issues (maintainer, the turn adr-45 ruling 3 deferred). Two branches minted `0055` and `0056` on the same day for different decisions, so the hand-numbered ordinal has the add/add collision the timestamp mint removed for captures. From here an ADR id is minted by the binary as `adr-<timestamp>` with a filename ordered by that stamp; `0001`–`0058` keep their ids and filenames, and nothing is renumbered. Rejected: minting ordinals from one checkout only (a convention, not a gate), and renumbering at merge (the collision tonight was found by reading, and a reader is not a gate). The mint verb and the lint rule that admits both shapes are implementation, tracked in the ledger.
- dcn-26091500000000092026-09-15 — The roles and loopback design workstream lands on main, executing the 2026-09-01 ruling that the branch waits for the mint verb and takes its ids at the merge. `abcd decide` now exists, so the two decisions the branch numbered 0055 and 0056 are re-minted as adr-2609151528057260 (three roles, who each artefact addresses, and when the loop stops) and adr-2609151528057131 (abcd owns the product thinker's surface), content, status and date unchanged; every citation that meant the roles decisions is re-pointed (rfc-3, the phase-8 page, the roles page, the out-of-scope list, three intent drafts, and iss-168, whose 29 August extension cited them by the colliding numbers), and main's own 0055 and 0056 keep their ids. The branch's twelve hand-numbered intent drafts (itd-165 to itd-176) collide with nothing and keep their ids, as adr-45's grandfathering allows. Occasion: itd-200 shipped today refining the two roles decisions, so their citations on main pointed at the wrong records until this landed.
- dcn-26101809134319852026-10-18 — The person's return shows two lists derived from what already exists (facilitator-on-delegation, itd-2610180912189125 planning): when the away mode ends, by its time passing or abcd mode back, the next prompt shows once the steps queued for the person's hands and the decisions recorded in the window, each with its text, which the reroute asks to carry its grounds and how to reverse it, plus any stop recorded with work owed. The decisions are the decision records whose time-ordered ids fall inside the window, so no second store of decisions is kept; the queue is the only new list and lives in the away record. The draft's richer lists for the person's return stay in itd-2610170638184486 (grounds: principle one-canonical-primitive; adr-45 time-ordered ids; itd-2610170638184486 press release)
- iss-330Parallel-mint id collision observed live: the maintainer's bughunt session and the release session each minted iss-291..293 concurrently; the collision surfaced only at the merge-queue's record-lint on the second branch, costing a full release re-cut. Direct field evidence for itd-114 (collision-proof record ids) — the ledger's next-free-integer mint is unsafe under exactly the parallel-agent workflows the repo now runs daily
- iss-2608210737260468The intent (itd) and spec (spc) families still mint via the legacy max+1 refs-union allocator; adr-45 ruling 3 schedules their adoption of the timestamp-numeric mint seam as configuration — a swap of the allocation call in the intent and spec stores onto the shared recordid seam, retiring their MaxAcrossRefs scans and mint warnings — after the captures family has run the scheme for a release cycle (itd-114 ac-6's Given clause). This is the tracker so the rollout's second family is not forgotten once that cycle completes
- iss-2608210737264758The optional forge-backed allocator (itd-114 ac-5) is deferred by recorded ruling — spc-33 names it a later adapter behind the same mint seam, adr-45 ruling 4 fixes its posture (allocates and never stores per itd-129's ledger-canonical line; offline falls back to native loudly) — but no open work item holds the seat, so nothing tracks building it. The fidelity verdict on itd-114 carries the criterion as NOT_MET/deferred-by-ruling; this capture is the deferred work's ledger seat, builds on itd-129's adapter seam when that lands
- iss-2608211143185943README.md described abcd capture as minting records 'with the next free id' — the proper name of the max+1 allocation protocol that adr-45/spc-33/itd-114 abolished after live collisions (one costing a release re-cut). 'Next free id' is the codebase's own name for the retired mechanism (recordid.MaxAcrossRefs, lint.go). The README is the only public document still describing it, setting the wrong mental model (small sequential successor-comparable ids) and legitimising the hand-add-at-max+1 path iss-74 identified as a collision source. Written 2026-08-17, pre-rollout.
- iss-2608211144555085The ADR index (.abcd/development/decisions/adrs/README.md) ends at the adr-44 row while adr-45 is accepted (2026-08-20) and cited as settled law by six live records (the invariants brief, itd-114, spc-33, .abcd/work/issues/README.md, two open issues, DECISIONS.md). 37 ADR files vs 36 index rows — exactly one missing. The index is the declared hand-maintained roll-call (adrs/README.md) and is not among index_drift's four registered indexes, so it is structurally ungated. Third recurrence of this class in the same table (after resolved iss-296 and iss-374), which iss-38's decision (hand-kept indexes must be deleted or gated) predicts.
- iss-2608220150157512ADRs, intents and specs still mint sequential ids (adr-NNNN, itd-N, spc-N), so two parallel branches minting in the same family produce add/add collisions on the same id — the exact mechanism adr-45 removed for issue captures, whose timestamp-numeric ids produced zero conflicts across twelve same-day captures on 2026-08-22 while the sequential families carry the standing risk. adr-45 rollout note 3 already names the path: one allocator, per-family adoption as configuration. Extending timestamp minting to the remaining families closes the class
- iss-2608220750029986adrs/README.md index omits the accepted adr-47 and adr-48 rows (site-build session) — fourth recurrence of the unindexed-ADR class after adr-45 (iss-38 wants these indexes gated); rows added in the same change that records this
- iss-2608221126066632Live recurrence, 2026-08-22: two concurrent sessions in two current checkouts each minted itd-144 and itd-145 in the same window — four records, two ids — resolved by hand-renumbering one side to itd-146/147. This is the second, undocumented path into the sequential-id collision recorded as iss-2608220150157512, which describes the stale-view path adr-45 already anticipated. Staleness was NOT the cause here: one session fast-forwarded 46 commits immediately before minting and still collided. The structural fact is that intents and specs mint max+1 (nextIntentID) while withIntentMintLock in internal/core/intent/create.go is advisory and scoped to a single checkout, so it cannot see a sibling worktree; two current checkouts therefore allocate the same id by construction. The durable fix is the timestamp mint adoption recorded as iss-2608210737260468. Recorded deliberately because capture has no add-evidence path and itd-87 (recurrence escalation) is unbuilt, so this evidence would otherwise exist only in two session transcripts.
- iss-2608221328552172abcd intent and abcd capture both swallow an unrecognised sub-verb as content and mint a record at exit 0, violating unrecognised-input-never-writes on the two highest-traffic mutating verbs. Verified on 45afe46 against a freshly built binary: 'abcd intent nosuchthing' prints 'created itd-148 (drafts)' and exits 0; 'abcd capture nosuchthing' prints 'captured iss-... (open)' and exits 0. Both records were durable files on disk, removed by hand afterwards. The principle at development/principles/unrecognized-input-never-writes.md states the rule -- a verb that mutates state fails closed, anything not exactly recognised is an error, never a fallback interpretation -- and cites this precise scenario as its founding evidence: a misspelled capture subcommand swallowed as capture text, filing an issue when the user asked to resolve one. The principle is therefore documented and unenforced, and the failure it was written about is still live on both verbs. The cause is structural rather than accidental: both verbs take free text as their canonical create path, so an unknown token is indistinguishable from intended content. That makes a bare token ambiguous by construction, which is the design question a fix must answer -- candidates include requiring quoting or an explicit create flag for the text path, refusing a single bare token that matches no sub-verb and resembles one, or accepting text only from stdin. A typo costs a spurious durable record, a burnt id under the max+1 intent allocator, and a record-lint index_drift blocker until the stray is noticed. Related: adr-45 on id allocation.
- iss-2608241740100762docs/explanation/process.md promises collision impossibility where the code provides improbability. It calls the minted issue id 'collision-proof (iss-<yymmddHHMMSS><4 random digits>) that no parallel agent can duplicate'. recordid/mint.go:53 records the real property: Mint reads nothing, so two minters cannot converge by sharing a stale view, and 'only a same-second suffix coincidence remains, which is the armed uniqueness detectors' residue to assert (adr-45 ruling 5)'. There is no read-back and no retry, so two agents minting in the same UTC second collide with probability 1e-4. AGENTS.md already documents the sequential-id collision hazard for intents and specs; this page overclaims for the timestamp mint.
- iss-2608300227228575IngestReading's on-disk collision probe checks only the current run's directory, so two runs ingested within one UTC second can mint one rdi-N in two run directories (the adr-45 mint is a second plus four digits, and nothing sequences ingests); afterwards that item can be neither dispositioned nor promoted, findReadingItem reports it present in more than one run, and no tree gate refuses it. The probe must be ledger-wide across every readings/rdg-*/ and redraw on a hit, and the findReadingItem comment claiming two runs can never mint one id is false.
- iss-2609012206053814Ruled 2026-09-01 ([dcn-2609010000000007](../../decisions/dcn-2609010000000007-adrs-take-timestamp-ids-through-the-same.md)): ADRs take timestamp ids through the same recordid seam as issues, because two branches minted 0055 and 0056 on the same day for different decisions. Implementation owed: a verb that mints adr-<timestamp> and writes the ADR skeleton under decisions/adrs with a filename ordered by the stamp; record-lint and the wiki-link resolver admit both the ordinal shape (0001-0058, kept as is) and the timestamp shape; the ADR index and site export sort ordinals first then stamps; adr-45's rollout note names this as the deferred family. Companion to the intent/spec adoption tracked by iss-2608210737260468.
- iss-2609090636110810AGENTS.md contradicts the decisions store on how ADR ids are minted, and the stale surface is the one agents read first. The root conventions file states that ADRs keep a hand-numbered filename ordinal and are the one record family where minting from two checkouts still needs a word first. The decide verb's own help says the opposite: the id is a timestamp-numeric stamp allocated through the shared record-id seam, so two branches deciding on the same day cannot collide, with the hand-numbered records grandfathered and every reader admitting both. The contradiction is not inert. In this session it caused a decision to be deferred that was safe to mint, on a coordination risk that adr-45 had already removed, and the deferral was written into a handover note as fact before it was checked. An agent reads the conventions file at the top of every session and reaches the verb's help only if it doubts what it just read.
- iss-2609100509524742A decomposition grade has no write path of its own, and the note it belongs in is a single shared file, so in a parallel run the grade cannot land at all.
- iss-2609111002410678`CLAUDE.md` tells every session that ADRs are the one record family still
- iss-2609211414208891A reading run id drawn twice in one second refuses the second run instead of redrawing. The run id is the family tag, a second-resolution stamp and a uniform four-digit draw (adr-45), and reading assemble derives the default output directory from it; when two assemblies in the same second draw the same suffix, the second finds the first's directory non-empty and exits 2 with the one-run-one-directory refusal, though nothing about the second run was wrong. The rehearsal eval assembles several runs within one second, so the one-in-ten-thousand coincidence per same-second pair is met in CI: TestRehearseTheOpeningRunLoop/empty-output-commits-a-clean-run failed on PR 651 at 12:09Z with rdg-2609211209406944 already holding two entries, on a docs-only branch whose local preflight had passed twice. adr-45 leaves the same-second coincidence to the armed uniqueness detectors to assert against, and here the detector is the empty-directory refusal, which asserts by refusing the innocent run. Wanted: when the run will land in the default directory, the mint redraws while the drawn directory exists (a bounded number of draws, then a refusal that names the collision), so the detector's residue is absorbed at the one site that can tell a collision from an operator's occupied directory; an operator-named --out that is non-empty keeps refusing as it does.
- iss-2610262031430108Invariant 11 forbids minting ids from the current maximum, but itd-44 has capture write `adr-<next-free>-<slug>.md`.
- itd-44A Capture-Time `decision` Verdict That Routes Infrastructure Choices Into The Existing ADR Store
- itd-86abcd Can Read Its Own Design Documents As A Stranger Would
- itd-148Every change starts in its own worktree in abcd's store: the primary checkout is a read-only surface, and abcd blocks mutations there for every session but a declared coordinator
- 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 unrecorded
- itd-180A cold reading's findings land as reading records, and the researcher's response is a separate disposition record — two acts, two writes, never collapsed
- itd-185One ingest verb validates every cold-reading output — including what the reading was licensed to produce, not only what it saw
- itd-187Amnesia is a repository property, proven by an eval — the same state assembled twice is byte-identical, so no case run is spent evidencing it
- itd-2609151138388536Every Decision Is Its Own Record, and the Log Stops Conflicting
- spc-33abcd-mints-collision-proof-record-ids-across-parallel-agents
- spc-55Claim typing and scope-condition identity
- spc-58Reading records and disposition records: two acts, two writes
- spc-60The lapse log: a capture category, timestamped at the lapse
- spc-61The cold-reading input assembler: positive inclusion at field granularity, a pathless bundle, and a hashed per-run manifest
- spc-63The cold-reading output contract: a strict per-position schema, a supply-regime gate, and a staged ingest that leaves evidence or nothing
- spc-67Step-2 admission records: schemas now, command enforcement next cycle
- spc-2610110931381316append-only-logs-conflict-on-every-merge
- spc-2610111627213138memory-recall