Row 90, round 2. Dewey built it, Filbert approved it (comment 28068) on manifest 95611274 (21 paths, baseb8539480). The approved PRD's requirement ids live in packages/tasks/requirements.json. task_create refuses an id outside it, and the PM session gets a list_requirements tool. Round 2 adds requirements.mjs to the sandbox's RUNNER_FILES (decision 127). Sage gate onc047d028plus the patch, 23:50:16Z to 23:54:26Z: tasks 89, bus 111, harness 66, webui 101, cli 123, seat 33, business 60, control-board 124, queue 150, conversation 182, discord 178, ledger 78, runs 43, mosaic 69, all fail 0. Every scripts/test-*.sh exited 0. Co-Authored-By: Claude Opus 5.5 <[email protected]>
54 lines
2.9 KiB
JavaScript
54 lines
2.9 KiB
JavaScript
import { readFileSync } from 'node:fs';
|
|
// Row 90 (#1584): the approved PRD's requirement ids as data, in ../requirements.json. The PRD
|
|
// (docs/prd/mosaic-stack.md) stays the reviewed source; tests/requirements.test.mjs fails unless
|
|
// the list holds its **REQ-...** ids in its order. A later PRD version is a new list in a reviewed
|
|
// commit. task.create refuses an id the list doesn't hold, and the broker's `requirements` verb
|
|
// returns the list to the sessions that create tasks. This module imports nothing from the bus,
|
|
// so the broker can load it. This function is the file's schema: it refuses any other shape.
|
|
export const REQUIREMENTS_FILE = new URL('../requirements.json', import.meta.url);
|
|
export const REQUIREMENT_ID = /^REQ-[A-Z]+-[1-9][0-9]*$/;
|
|
export class RequirementsError extends Error {}
|
|
const plain = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
const str = (x, max) => typeof x === 'string' && x.trim() !== '' && x.length <= max;
|
|
function exact(x, where, allowed) {
|
|
if (!plain(x)) throw new RequirementsError(`${where}: not an object`);
|
|
for (const k of Object.keys(x)) if (!allowed.includes(k)) throw new RequirementsError(`${where}: unknown key ${k}`);
|
|
for (const k of allowed) if (!Object.hasOwn(x, k)) throw new RequirementsError(`${where}: missing ${k}`);
|
|
}
|
|
export function checkRequirements(list) {
|
|
exact(list, 'list', ['prd', 'version', 'requirements']);
|
|
if (!str(list.prd, 256) || !/^docs\/prd\/[a-z0-9-]+\.md$/.test(list.prd))
|
|
throw new RequirementsError('prd: not a docs/prd/*.md path');
|
|
if (typeof list.version !== 'string' || !/^[1-9][0-9]*\.[0-9]+$/.test(list.version))
|
|
throw new RequirementsError('version: not a version such as 1.0');
|
|
if (!Array.isArray(list.requirements) || !list.requirements.length)
|
|
throw new RequirementsError('requirements: not a nonempty list');
|
|
const seen = new Set();
|
|
for (const [i, r] of list.requirements.entries()) {
|
|
const at = `requirements[${i}]`;
|
|
exact(r, at, ['id', 'title']);
|
|
if (typeof r.id !== 'string' || !REQUIREMENT_ID.test(r.id)) throw new RequirementsError(`${at}.id: not a requirement id`);
|
|
if (seen.has(r.id)) throw new RequirementsError(`${at}.id: ${r.id} twice`);
|
|
seen.add(r.id);
|
|
if (!str(r.title, 120) || r.title.includes('\n')) throw new RequirementsError(`${at}.title: not one line of 120 characters or fewer`);
|
|
}
|
|
return list;
|
|
}
|
|
function freeze(x) {
|
|
if (x && typeof x === 'object') {
|
|
for (const v of Object.values(x)) freeze(v);
|
|
Object.freeze(x);
|
|
}
|
|
return x;
|
|
}
|
|
// The committed list, checked and frozen. A file that fails the check refuses to load.
|
|
export function loadRequirements(file = REQUIREMENTS_FILE) {
|
|
let list;
|
|
try {
|
|
list = JSON.parse(readFileSync(file, 'utf8'));
|
|
} catch (e) {
|
|
throw new RequirementsError(e instanceof SyntaxError ? 'requirements.json: not JSON' : `requirements.json: ${e.code ?? 'unreadable'}`);
|
|
}
|
|
return freeze(checkRequirements(list));
|
|
}
|