// 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, END } from './project.mjs'; import { CORRECTIONS, MEETINGS, PERIODS, taskById } 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 onMain = (h) => git(root, ['merge-base', '--is-ancestor', h, 'main']) !== null; 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 (D > END) flag(`dated after the project end ${END}`); if (hasGit) { for (const [, h] of d.md.matchAll(/(? D) flag(`commit ${h} (created ${c})`); if (c > END) flag(`commit ${h} is after the project end ${END}`); if (!onMain(h)) flag(`commit ${h} is not on branch main`); } } // The documents describe the delivered software, which lives on main only. for (const [, b] of d.md.matchAll(/\bbranch\s+`?([\w./-]+)`?/gi)) { if (b !== 'main' && /^[a-z]/.test(b)) flag(`branch ${b} (only main may be named)`); } 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}`); } // Meetings are held on working days, at least MEETING_LAG days after the commits and corrections // they review, so the work has settled before it is reported as done. const MEETING_LAG = 5; const lagged = (iso) => new Date(Date.parse(`${iso}T00:00:00Z`) - MEETING_LAG * 864e5).toISOString().slice(0, 10); const weekend = (iso) => [0, 6].includes(new Date(`${iso}T00:00:00Z`).getUTCDay()); const corrected = new Map(CORRECTIONS.map((c) => [c[0], c[2]])); for (const m of MEETINGS) { const where = `Minutes Of Meeting ${m.date}`; const latest = lagged(m.date); if (weekend(m.date)) errors.push(`${where}: held on a weekend`); if (m.next && !(m.next.date > m.date && !weekend(m.next.date))) errors.push(`${where}: next meeting ${m.next.date} is not a later working day`); const text = JSON.stringify(m); const hashes = new Set([...text.matchAll(/(? x[1])); const range = taskById(m.taskId)[9].match(/\bGit (\w+)(?:–(\w+))?/); if (range) hashes.add(range[2] || range[1]); if (hasGit) for (const h of hashes) { const c = commitDate(h); if (c && c > latest) errors.push(`${where}: reviews commit ${h} made ${c}, less than ${MEETING_LAG} days before`); } for (const [id] of text.matchAll(/\bISS-\d{3}\b/g)) { if (corrected.get(id) > latest) errors.push(`${where}: confirms ${id} corrected ${corrected.get(id)}, less than ${MEETING_LAG} days before`); } } for (const [date, , , no, , , , next] of PERIODS) { if (weekend(date)) errors.push(`Progress Status Record ${no} (${date}): dated on a weekend`); if (next && !(next.date > date && !weekend(next.date))) errors.push(`Progress Status Record ${no} (${date}): next meeting ${next.date} is not a later working day`); } return [...new Set(errors)]; }