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:
Thanakorn
2026-09-25 14:52:21 +07:00
co-authored by Claude Opus 5.5
parent be020e332b
commit 6c857005c0
144 changed files with 367 additions and 274 deletions
+74
View File
@@ -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;
}