/** * ADR slug auto-allocation chokepoint. * * ## Why this module exists * * Before T10360, `cleo docs add --type adr` required the caller to * hand-craft an `adr-NNNN-` slug. Agents and humans both * (a) had to discover the next available ADR number by grepping disk * or `.cleo/adrs/` and (b) frequently picked colliding numbers when * multiple agents drafted ADRs concurrently. That was the user-facing * symptom T10153 originally tracked. * * This module centralises the allocation: callers provide `--type adr` * + `--title ""` and the allocator: * * 1. Queries the docs SSoT for the highest existing ADR number. * 2. Increments by 1 and assembles the candidate slug * `adr--` (`NNN` zero-padded to width 3 — the * ADR numbering convention used across `.cleo/adrs/` and the * `docs/adr/` publish dir). * 3. Calls {@link reserveSlug} (the T10392 central allocator * chokepoint) so the slug is reserved in the in-process Mutex * map AND probed against the `uniq_attachments_slug` partial * UNIQUE INDEX in a single chokepoint. * 4. On `E_SLUG_RESERVED` (rare — two agents racing for the same * number despite the per-slug Mutex), retries with `N+1`, * `N+2`, ... up to {@link MAX_ADR_ALLOCATION_ATTEMPTS} before * surfacing `E_ADR_NUMBER_EXHAUSTED`. * * The allocator is project-scoped: it asks `.cleo/tasks.db` for the * highest existing ADR number, NOT a global registry. Different * projects keep independent ADR sequences. * * ## Why width 3 (`adr-001-...`) * * The historical `.cleo/adrs/` directory uses `ADR-NNN-.md` * filenames with `NNN` zero-padded to 3 digits (e.g. * `ADR-083-cleo-persona.md`). Auto-allocated slugs preserve that * width so the slug ↔ filename mapping stays mechanically obvious * for humans and orchestrators alike. The cap at 999 is a soft * ceiling — ADRs that overflow it bump to 4 digits naturally * (`adr-1000-...`) because the regex tolerates any digit length. * * ## Bypass * * Callers passing an explicit `--slug` skip the allocator entirely * and are routed straight to {@link reserveSlug} on the provided * value. This preserves backward compatibility with * `ct-adr-recorder`'s historical recipe and lets HITL operators * pick a specific number when superseding a deleted/withdrawn ADR. * * @task T10360 * @epic T10291 (E3-DOCS-CLI-HARDENING) * @saga T10288 (SG-DOCS-INTEGRITY) * @closes T10153 */ import { type ReserveSlugOptions, reserveSlug } from './slug-allocator.js'; /** * Maximum number of consecutive allocation attempts before giving up. * * In practice the per-slug Mutex + DB UNIQUE INDEX serialises * reservations so a single increment is almost always enough. The * cap defends against pathological concurrency races (e.g. 5+ agents * racing on the same epoch) by surfacing `E_ADR_NUMBER_EXHAUSTED` * rather than retrying forever. */ export declare const MAX_ADR_ALLOCATION_ATTEMPTS = 5; /** * Successful ADR allocation outcome. * * `number` is the integer ADR id that was allocated. `slug` is the * canonical form written to `attachments.slug`. `slug` matches * `adr--` for `number <= 999` and * `adr--` for `number >= 1000`. */ export interface AdrAllocateOk { readonly ok: true; readonly number: number; readonly slug: string; } /** * Failed ADR allocation outcome. * * `code` discriminates between exhaustion (5+ reservation collisions * in a row — `E_ADR_NUMBER_EXHAUSTED`) and validation failures * (`E_VALIDATION` for an empty title that slugifies to nothing). The * dispatch layer maps `code` straight into the LAFS error envelope. */ export interface AdrAllocateErr { readonly ok: false; readonly code: 'E_ADR_NUMBER_EXHAUSTED' | 'E_VALIDATION'; readonly message: string; } /** Discriminated union returned by {@link allocateAdrSlug}. */ export type AdrAllocateResult = AdrAllocateOk | AdrAllocateErr; /** * Params accepted by {@link allocateAdrSlug}. * * Follows the canonical Core SSoT dispatch contract (ADR-057): the * function takes `(projectRoot, params)` so it can be invoked from a * `defineTypedHandler` block without the SSoT lint flagging it as a * non-uniform signature. */ export interface AllocateAdrParams extends ReserveSlugOptions { /** * Human-readable title — slugified into the slug tail. * Empty / whitespace-only titles return `E_VALIDATION`. */ readonly title: string; /** * Override the starting ADR number. Used by unit tests to force the * allocator down the retry path without populating the DB first. * * @internal */ readonly startNumberOverride?: number; /** * Override the reserveSlug implementation. Used by unit tests to * simulate persistent contention without hitting the real DB. * * @internal */ readonly reserveSlugImpl?: typeof reserveSlug; } /** * @deprecated Legacy positional shape — retained for one release of * back-compat with internal callers. Prefer {@link AllocateAdrParams}. */ export interface AllocateAdrOptions extends ReserveSlugOptions { readonly startNumberOverride?: number; readonly reserveSlugImpl?: typeof reserveSlug; } /** * Find the highest ADR number currently recorded in the attachments * table. * * Returns `0` when no ADR slugs exist yet (so the first allocation * lands on `1` — `adr-001-`). * * @param cwd - Optional working directory for `.cleo/` resolution. * @returns Highest ADR number, or `0` when none exist. * @internal — exported for tests. */ export declare function findHighestAdrNumber(cwd?: string): Promise; /** * Assemble the canonical ADR slug from a number and a free-form title. * * `number` is zero-padded to width 3 (the historical convention) when * it fits, otherwise the natural-width string is used so `adr-1000-foo` * stays well-formed. * * `title` is slugified via the shared {@link slugify} helper so the * shape matches existing T9636 conventions and the * `uniq_attachments_slug` regex constraints. * * @param number - The ADR number (>= 1). * @param title - Raw human-readable title (e.g. "Adopt Drizzle v1 beta"). * @returns Canonical slug (e.g. `adr-042-adopt-drizzle-v1-beta`). * @internal — exported for tests. */ export declare function assembleAdrSlug(number: number, title: string): string; /** * Auto-allocate an ADR slug for an upcoming `attachmentStore.put` call. * * Returns a discriminated union: on success the slug has been reserved * via {@link reserveSlug} and the caller MUST proceed with `put` (or * release explicitly via `releaseReservedSlug`). On failure the slug * has NOT been reserved and the dispatch layer surfaces the error * envelope verbatim. * * The allocator retries on `E_SLUG_RESERVED` up to * {@link MAX_ADR_ALLOCATION_ATTEMPTS} times. Each retry increments * the candidate number — concurrent allocators converge on distinct * slugs without re-querying the DB on every attempt. * * @param title - Human-readable title — slugified into the slug tail. * Empty / whitespace-only titles return `E_VALIDATION`. * @param opts - Optional cwd + test overrides. * @returns `{ ok: true, number, slug }` or `{ ok: false, code, message }`. * * @param projectRoot - Absolute path to the project root (the `.cleo/` * parent). Used for DB resolution + slug * reservation. Pass `getProjectRoot()` from the * dispatch layer. * @param params - Allocation params — `title` is the canonical kebab * source. Test seams (`startNumberOverride`, * `reserveSlugImpl`) are optional. * @returns `{ ok: true, number, slug }` or `{ ok: false, code, message }`. * * @task T10360 */ export declare function allocateAdrSlug(projectRoot: string, params: AllocateAdrParams): Promise; //# sourceMappingURL=adr-allocator.d.ts.map