// Hindsight guard: a work product may only cite what existed on its own date. // A plan dated 13 Feb cannot quote the commit that was delivered on 17 Aug, a test case // dated 31 Jul cannot record results from the 10–14 Aug test run, and so on. // Planned dates in the future are fine; facts that only came into being later are not. import { execFileSync } from 'node:child_process'; import { TAG } from './project.mjs'; import { CORRECTIONS } from './history.mjs'; /** Document date from its name: "... 25690817 V1.0 ..." -> 2026-08-17 */ export function docDate(name) { const m = name.match(/ (25\d\d)(\d\d)(\d\d) V/); if (!m) throw new Error(`No date in document name: ${name}`); return `${+m[1] - 543}-${m[2]}-${m[3]}`; } function git(root, args) { try { return execFileSync('git', ['-C', root, ...args], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch { return null; } } /** * Identifier families and the work product that first defines them. An ID may be cited only * on or after the date of that work product. */ const FAMILIES = [ { label: 'Customer Requirement', re: /\bCR\d\d(?::\d{3})?\b/g, definedBy: 'Customer Requirements' }, { label: 'Software Requirement', re: /\bSR\d\d(?::\d{3})?\b/g, definedBy: 'Software Requirements' }, { label: 'Software Unit', re: /(? { if (!commitDates.has(h)) commitDates.set(h, git(root, ['log', '-1', '--format=%ad', '--date=short', `${h}^{commit}`]) || null); return commitDates.get(h); }; const tagDate = hasGit ? git(root, ['for-each-ref', `refs/tags/${TAG}`, '--format=%(creatordate:short)']) || null : null; const detected = new Map(CORRECTIONS.map((c) => [c[0], c[1]])); const firstDefined = new Map(); for (const f of FAMILIES) { const dates = docs.filter((d) => d.name.includes(` ${f.definedBy} `)).map((d) => docDate(d.name)).sort(); if (!dates.length) throw new Error(`No work product defines ${f.label} IDs (${f.definedBy})`); firstDefined.set(f, dates[0]); } for (const d of docs) { const D = docDate(d.name); const found = new Set(); const flag = (what) => found.add(what); if (hasGit) { for (const [, h] of d.md.matchAll(/(? D) flag(`commit ${h} (created ${c})`); } } if (d.md.includes(TAG) && !(tagDate && tagDate <= D)) flag(`tag ${TAG} (${tagDate ? `created ${tagDate}` : 'not created yet'})`); for (const [id] of d.md.matchAll(/\bISS-\d{3}\b/g)) { const at = detected.get(id); if (!at) flag(`${id} is not in the Correction Register`); else if (at > D) flag(`${id} (detected ${at})`); } for (const f of FAMILIES) { const at = firstDefined.get(f); if (at <= D) continue; const ids = [...new Set([...d.md.matchAll(f.re)].map((m) => m[0]))]; if (ids.length) flag(`${f.label} IDs ${ids.slice(0, 3).join(', ')}${ids.length > 3 ? ', …' : ''} (defined ${at})`); } for (const what of found) errors.push(`${d.name}: ${what}`); } return errors; }