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)); }