docs(sdlc): write each work product as of its own date
- 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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
be020e332b
commit
6c857005c0
@@ -0,0 +1,74 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user