- SOW, plan and design no longer cite the Aug baseline commit or the package tag - Work Schedule status derived from its date; Test Case has no results yet - build fails when a document cites a later commit, tag, ISS or ID (timeline.mjs) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
75 lines
3.5 KiB
JavaScript
75 lines
3.5 KiB
JavaScript
// 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: /(?<!TC-)\bUN\d\d(?:\.\d{3})?\b/g, definedBy: 'Software Design' },
|
||
{ label: 'Test Case', re: /\bTC-UN\d\d\.\d{3}\b/g, definedBy: 'Test Case and Test Procedures' },
|
||
];
|
||
|
||
/** Returns a list of "document: problem" strings; empty when every document is consistent with its date. */
|
||
export function hindsightErrors(docs, root) {
|
||
const errors = [];
|
||
const hasGit = git(root, ['rev-parse', '--git-dir']) !== null;
|
||
const commitDates = new Map();
|
||
const commitDate = (h) => {
|
||
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(/(?<![\w/.-])([0-9a-f]{7,40})(?![\w/.-])/g)) {
|
||
const c = commitDate(h);
|
||
if (c && c > 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;
|
||
}
|