/** * Serialized ISS-NNNN and EPIC-NNNN allocation. * * The defect this closes: `create_issue` picked the next id by scanning the * issues directory for the current maximum, then wrote the file many awaits * later. Two processes could read the same maximum and both write ISS-0141. * Four duplicate-id groups were produced by exactly this and were found on disk * (ISS-0015, ISS-0028, ISS-0054, ISS-0112), reported independently by the * `machina` peer and reproduced locally. THIS repo's four were cleared: as of * 2026-09-19 it holds 200 issues and 38 epics with ZERO duplicate frontmatter * ids. ISS-0136 remains OPEN, because it is portfolio-wide and 61 groups still * sit in five other projects (frontend_v0.2, senken-trading-agent, machina, * decibel-studio, decibel-tools-mobile). Do not read "clean" here as "clean * everywhere". The history is kept because it is the evidence * this lock exists, not because the state persists — stated in the past tense * deliberately, since the present-tense version outlived the fact and was read * as current by an outside reader, who repeated it in a document. * * Verify by parsing frontmatter, not by grepping. `grep "^id: ISS-"` over this * store counts documents that QUOTE an id — a report about malformed * frontmatter contains the line it is reporting — and that false positive has * already cost one confidently wrong correction. * * Two independent defences, because a lock only protects callers that take it: * * 1. The lock spans **allocation through successful write**, not just the id * calculation. Serializing only the scan leaves the same race — both * processes still compute 141 before either file lands. * * 2. The write itself is exclusive. If a caller bypasses the lock entirely — * an old client, a migration script, a future mistake — a colliding write * fails loudly with EEXIST instead of silently overwriting a real issue. * It is also atomic (ISS-0195): the record is published by link(2) from a * complete, fsynced temp, so a SIGKILL mid-write can no longer leave a * 0-byte record under a real id, which plain O_EXCL could. * * Both `createIssue` implementations (src/tools/sentinel.ts and * src/sentinelIssues.ts) allocate from a single numbering space and write * different formats into the same directory, so they must share this path or * they race against each other. */ /** The record kinds that carry an allocated NNNN. */ export type RecordPrefix = 'ISS' | 'EPIC'; export declare function formatRecordId(prefix: RecordPrefix, num: number): string; export declare function formatIssueId(num: number): string; /** * Highest PREFIX-NNNN currently present, by filename prefix or frontmatter id. * * Both sources are checked because the two record formats disagree: PREFIX-NNNN * files carry the id in the name, while timestamp-slug records carry it only in * frontmatter. Scanning one source alone under-counts and hands out an id that * is already taken. */ export declare function scanMaxRecordNumber(dir: string, prefix: RecordPrefix): Promise; export declare function scanMaxIssueNumber(issuesDir: string): Promise; export interface AllocatedRecord { id: string; filename: string; filePath: string; } /** * Allocate the next PREFIX-NNNN and write the record, with both held under one * lock so no other process can allocate the same id in between. * * `buildContent` receives the allocated id because the id appears inside the * record body (frontmatter `id:`), so content cannot be built before allocation. * * On the rare EEXIST — a bypassing writer took the id between our scan and our * write — the allocation is retried rather than failed, up to `maxAttempts`. * Retrying is correct here: the id is genuinely taken, and the next one up is a * valid answer to the caller's request. */ export declare function allocateAndWriteRecord(dir: string, prefix: RecordPrefix, extension: 'md' | 'yml', slug: string, buildContent: (recordId: string) => string, options?: { maxAttempts?: number; timeoutMs?: number; /** Called with the resolved path before each write attempt, to reject a * path that escapes the project. Throwing aborts the allocation. */ validate?: (filePath: string) => void; }): Promise; /** ISS-NNNN allocation. See {@link allocateAndWriteRecord}. */ export declare function allocateAndWriteIssue(issuesDir: string, extension: 'md' | 'yml', slug: string, buildContent: (issueId: string) => string, options?: { maxAttempts?: number; timeoutMs?: number; validate?: (filePath: string) => void; }): Promise; /** * EPIC-NNNN allocation. * * Added because `logEpic` had the pre-lock defect verbatim: it read the * directory for the current maximum, then wrote the file ~20 awaits later. * Epics are the worse half of that race. An issue collision at least left two * files and a visible duplicate id; the epic path wrote through * `writeFileAtomic`, whose rename CLOBBERS, so the loser was not duplicated — * it was gone, and its caller had already been told it was created. Found by * the S5 concurrency sweep. */ export declare function allocateAndWriteEpic(epicsDir: string, slug: string, buildContent: (epicId: string) => string, options?: { maxAttempts?: number; timeoutMs?: number; validate?: (filePath: string) => void; }): Promise; //# sourceMappingURL=recordIdAllocator.d.ts.map