Files
wms-app/scripts/sdlc-seed/timeline.mjs
T

84 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 } 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 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(/(?<![\w/.-])([0-9a-f]{7,40})(?![\w/.-])/g)) {
const c = commitDate(h);
if (!c) continue;
if (c > 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}`);
}
return errors;
}