fix(story-lifecycle): reject unsafe numeric allocation IDs

A real CLI fixture reproduced an infinite allocation loop from an existing unsafe numeric ID. Reject unsafe IDs and overflow without writing markers; 26 workflow fixtures, changed-code coverage and validators pass.
This commit is contained in:
haelyra
2026-10-11 02:17:50 -04:00
parent 52d546f0d1
commit 1bbebf61d9
3 changed files with 23 additions and 2 deletions
@@ -10,7 +10,7 @@
"id": "hand-seeded",
"source": "manual-curation-pending-regeneration"
},
"registryDigest": "abd597fc032cfab681b4083339491070034f86c947503194499b82771daada8a",
"registryDigest": "6a6a4fd5cc80ecda18184f6b35625c99ccc1bb0036c5e30316fa179ae81e451f",
"schemaVersion": 1,
"triggers": {
"skill:agent-runtime-gateway-smoke-test": [
+6 -1
View File
@@ -232,10 +232,15 @@ function allocate(root, dir, epic, count) {
fs.mkdirSync(reg, { recursive: true });
const taken = [...fs.readdirSync(dir), ...fs.readdirSync(reg).map((f) => `${f}.md`)]
.map((f) => f.match(STORY_FILE)).filter((m) => m && m[1] === epic).map((m) => Number(m[2]));
if (taken.some((n) => !Number.isSafeInteger(n))) {
throw new LedgerError('existing story ID is not a safe integer');
}
let n = Math.max(0, ...taken);
const ids = [];
while (ids.length < count) {
const id = `${epic}-${++n}`;
n += 1;
if (!Number.isSafeInteger(n)) throw new LedgerError('story ID exceeds safe integer range');
const id = `${epic}-${n}`;
try {
fs.writeFileSync(path.join(reg, id), '', { flag: 'wx' });
ids.push(id);
+16
View File
@@ -380,6 +380,22 @@ fs.readFileSync = function (p, ...a) { const r = orig.call(fs, p, ...a);
}
}],
['allocation rejects unsafe existing numeric IDs instead of looping forever', () => {
const root = newRepo();
const registry = path.join(root, '.git', 'story-lifecycle', 'ids');
fs.mkdirSync(registry, { recursive: true });
for (const n of ['9007199254740992', '9007199254740991']) {
const marker = path.join(registry, `auth-flow-${n}`);
fs.writeFileSync(marker, '');
const result = spawnSync(process.execPath, [LEDGER, 'allocate', '--root', root,
'--epic', 'auth-flow', '--count', '2'], { encoding: 'utf8', timeout: 2000 });
assert.strictEqual(result.status, 2, result.stderr || result.error?.message);
assert.match(result.stderr, /safe integer/);
assert.deepStrictEqual(fs.readdirSync(registry), [`auth-flow-${n}`]);
fs.rmSync(marker);
}
}],
['ledger paths are confined to .stories/ file shapes', () => {
const dir = path.join(os.tmpdir(), '.stories');
for (const bad of ['../x-1.md', 'epics/../../x.md', 'Auth-1.md', 'a/b-1.md', 'epics/a.txt', 'sprints/sprint-0.md', '/abs-1.md']) {