* test(6): reproduce W007 + phaseVariants + W006 drift between CJS verify and SDK validate Adds tests/6-validate-cjs-drift-regression.test.cjs with 5 RED tests covering the three drift items from issue #6 between verify.cjs (Check 8) and validate.ts (Check 8): 1. W007 activeDiskPhases — verify.cjs uses diskPhases (includes archived) for W007; archived phase "1" absent from current ROADMAP fires false W007. validate.ts: activeDiskPhases (active phasesDir only) correctly excludes archives. 2. phaseVariants() normalization — ROADMAP says "01A", disk has "1A-foo". verify.cjs parseInt("01A")=1 → padded "01" (drops letter suffix) → miss. validate.ts phaseVariants("01A") = {"01A","1A","01A"} → "1A" matched. Both W006 and W007 fire as false positives in verify.cjs. 3. W006 letter-suffix padding mismatch — ROADMAP says "3B", disk has "03B-foo". verify.cjs parseInt("3B")=3 → padded "03" (drops "B") → diskPhases.has("03B") missed. W006 and W007 fire as false positives. All 5 tests RED on origin/main. Will turn GREEN after generator + verify.cjs migration. References: - Issue #6 (open-gsd/get-shit-done-redux) — maintainer acceptance criteria: "Port all three items to verify.cjs; add parity tests confirming identical output for all three cases on both paths" - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - PR #154 (issue #4) — precedent for the generator pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(6): add sdk/scripts/gen-validate.mjs generator Extracts phaseVariants() from sdk/dist/query/validate.js via brace-balanced source-text parsing (phaseVariants is a closure inside validateHealth, not a module export, so Function.prototype.toString() is unavailable). Emits get-shit-done/bin/lib/validate.generated.cjs with three pure helpers: - phaseVariants(phase): normalized Set of padded/unpadded/letter-suffix variants - buildRoadmapPhaseVariants(content): {roadmapPhases, roadmapPhaseVariants} - buildNotStartedPhaseVariants(content): Set of unchecked-phase variants These three helpers directly address the three drift items in issue #6. Follows the gen-phase-lifecycle-policy.mjs extraction pattern from PR #154. References: - Issue #6 (open-gsd/get-shit-done-redux) - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - PR #154 (issue #4) — generator pattern precedent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(6): add sdk/scripts/check-validate-fresh.mjs freshness check Mirrors check-phase-lifecycle-policy-fresh.mjs from PR #154: imports buildValidateCjs() directly, regenerates in-memory, and diffs against the committed validate.generated.cjs. Exits 1 if stale (CI gate). References: - Issue #6 (open-gsd/get-shit-done-redux) - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - PR #154 (issue #4) — precedent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(6): emit validate.generated.cjs from validate.ts Generated by: node sdk/scripts/gen-validate.mjs Exports three pure helpers extracted from sdk/src/query/validate.ts Check 8: - phaseVariants(phase): Set of normalized variants {"01A","1A"} etc. - buildRoadmapPhaseVariants(content): {roadmapPhases, roadmapPhaseVariants} - buildNotStartedPhaseVariants(content): Set of unchecked-phase variants Freshness check: node sdk/scripts/check-validate-fresh.mjs → FRESH References: - Issue #6 (open-gsd/get-shit-done-redux) - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - PR #154 (issue #4) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(6): migrate verify.cjs to consume validate.generated.cjs helpers (GREEN) Check 8 in verify.cjs now uses three generated helpers from validate.generated.cjs: 1. buildRoadmapPhaseVariants(roadmapContent) — replaces hand-rolled roadmapPhases Set. Produces both roadmapPhases (raw, for W006 message) and roadmapPhaseVariants (all variants, for W007 membership check). Fixes false W007 for letter-suffix phases with padding mismatch. 2. activeDiskPhases — now uses collectDiskPhases() WITHOUT forEachArchivedPhaseToken. W007 iterates activeDiskPhases, not diskPhases, so archived phases absent from current ROADMAP no longer trigger false W007. 3. buildNotStartedPhaseVariants(roadmapContent) — replaces raw+parseInt-padded notStartedPhases population. Uses phaseVariants() expansion so zero-padded letter-suffix unchecked entries (e.g. "03B") correctly suppress W006 for their un-padded counterpart ("3B") and vice versa. 4. phaseVariants() in W006 loop — replaces parseInt-padded disk-existence check. "3B" now matches disk dir "03B-foo" via variant expansion. Also updates test fixture for drift item 1 to use two milestone archives (v1.0 + v1.1), accurately reproducing the scenario where forEachArchivedPhaseToken walks ALL archives while getActiveMilestoneArchiveDir returns only the most recent one. All 5 tests GREEN. Confirmed RED on pre-fix code (git stash test). References: - Issue #6 (open-gsd/get-shit-done-redux) — maintainer acceptance criteria: "Port all three items to verify.cjs; add parity tests confirming identical output" - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - PR #154 (issue #4) — generator pattern precedent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(6): wire validate freshness check into test workflow Adds 'SDK generated validate artifact drift check' step to .github/workflows/test.yml, mirroring the pattern used by all PR #154 generator freshness checks. Runs on ubuntu-latest/node-24 only (same as other artifact drift checks). Placement: after workstream-name-policy check, before Shared Module hand-sync drift check. References: - Issue #6 (open-gsd/get-shit-done-redux) - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - PR #154 (issue #4) — precedent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(6): wire gen:validate into sdk/package.json, root package.json, and allowlist sdk/package.json: adds gen:validate and check:validate-fresh npm scripts. package.json: adds check:validate-fresh script (mirrors other check:*-fresh entries). scripts/shared-module-handsync-allowlist.json: updates verify.cjs justification to note that Check 8 W006/W007 helpers are now generated from validate.ts via gen-validate.mjs (issue #6), with freshness check at check-validate-fresh.mjs. References: - Issue #6 (open-gsd/get-shit-done-redux) - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(6): amend ADR-3524 — validate.ts now uses generator pattern Adds 2026-05-23 amendment section to docs/adr/3524-cjs-sdk-hard-seam.md documenting: - Generator/artifact/freshness-check/CI paths - Three drift items resolved (W007 activeDiskPhases, phaseVariants normalization, W006 unchecked-phase variant skip) - phaseVariants extraction technique (brace-balanced source-text parsing) - Parity test coverage (5 tests, RED→GREEN) - Allowlist classification preserved (cooperating-sibling) References: - Issue #6 (open-gsd/get-shit-done-redux) - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - PR #154 (issue #4) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(6): add changeset fragment for validate.ts/verify.cjs generator migration Touches get-shit-done/bin/lib/validate.generated.cjs and verify.cjs which match USER_FACING_PREFIXES. Required by the fix-template checklist + the changeset-lint CI workflow. Refs #6 #156 * docs(6): register validate.generated.cjs in INVENTORY + manifest INVENTORY parity test demanded a row for the new generated CJS surface and a matching entry in INVENTORY-MANIFEST.json. Headline count bumped from 74 → 75. Refs #6 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
171 lines
6.4 KiB
JavaScript
171 lines
6.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Generator for the Validate CJS artifact.
|
|
*
|
|
* Reads the compiled ESM output from sdk/dist/query/validate.js, extracts the
|
|
* pure `phaseVariants` helper function via source-text extraction (it is a
|
|
* closure inside validateHealth, not a module-level export), then emits
|
|
* get-shit-done/bin/lib/validate.generated.cjs.
|
|
*
|
|
* The generated module exports three pure helpers that were missing from
|
|
* verify.cjs (the three drift items from issue #6):
|
|
*
|
|
* 1. phaseVariants(phase) — generates all normalized variants of a phase
|
|
* token (padded/unpadded/letter-suffix). Used for W006 disk-existence check
|
|
* and W007 roadmap-membership check in verify.cjs Check 8.
|
|
*
|
|
* 2. buildRoadmapPhaseVariants(roadmapContent) — parses ROADMAP.md and builds
|
|
* the Set of all variants of all roadmap phases. Used by the W007
|
|
* check. verify.cjs previously used only raw phase tokens (no variants).
|
|
*
|
|
* 3. buildNotStartedPhaseVariants(roadmapContent) — parses ROADMAP.md unchecked
|
|
* phase entries and builds a Set of all variants. Used for the W006
|
|
* unchecked-phase skip. verify.cjs previously added only raw+zero-padded
|
|
* (dropping letter suffix via parseInt).
|
|
*
|
|
* Extraction approach: since phaseVariants is defined as a closure inside
|
|
* validateHealth (not a module export), it is extracted from the compiled source
|
|
* text using a brace-balanced parser — the same approach used to extract
|
|
* escapeRegex in gen-phase-lifecycle-policy.mjs.
|
|
*
|
|
* Run: cd sdk && npm run gen:validate
|
|
* Check: node sdk/scripts/check-validate-fresh.mjs
|
|
*
|
|
* References:
|
|
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
|
* - Issue #6 (open-gsd/get-shit-done-redux)
|
|
* - PR #154 (issue #4) — generator pattern precedent
|
|
*/
|
|
|
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
export const BANNER = `'use strict';
|
|
|
|
/**
|
|
* GENERATED FILE — DO NOT EDIT.
|
|
*
|
|
* Source: sdk/src/query/validate.ts
|
|
* Regenerate: cd sdk && npm run gen:validate
|
|
*
|
|
* Validate Helpers — pure computation helpers for phase variant normalization,
|
|
* roadmap phase variant set construction, and unchecked-phase skip set construction.
|
|
* No I/O. No async. No filesystem operations.
|
|
*
|
|
* These three helpers cure the three drift items from issue #6:
|
|
* 1. phaseVariants() — replaces parseInt-based padded/unpadded check in verify.cjs
|
|
* Check 8 (W006 disk-existence and W007 roadmap-membership checks).
|
|
* 2. buildRoadmapPhaseVariants() — replaces raw roadmapPhases set in W007 loop.
|
|
* 3. buildNotStartedPhaseVariants() — replaces raw+zero-padded notStartedPhases
|
|
* in W006 skip logic.
|
|
*
|
|
* I/O adapter pattern (ADR-3524 §4): pure transforms extracted from the SDK.
|
|
*
|
|
* References:
|
|
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
|
* - Issue #6 (open-gsd/get-shit-done-redux)
|
|
* - PR #154 (issue #4) — generator pattern precedent
|
|
*/
|
|
|
|
`;
|
|
|
|
/**
|
|
* Extract phaseVariants from compiled validate.js source text.
|
|
*
|
|
* phaseVariants is defined as a const arrow function closure inside validateHealth.
|
|
* It starts with the literal `const phaseVariants = (phase) => {` and ends at the
|
|
* matching closing brace. We re-emit it as a standalone named function declaration.
|
|
*/
|
|
function extractPhaseVariantsBody(validateSource) {
|
|
const marker = 'const phaseVariants = (phase) => {';
|
|
const start = validateSource.indexOf(marker);
|
|
if (start === -1) throw new Error('Could not find phaseVariants in compiled validate.js');
|
|
|
|
// Find the opening brace of the arrow function body
|
|
const braceOpen = validateSource.indexOf('{', start + marker.length - 1);
|
|
let depth = 0;
|
|
let i = braceOpen;
|
|
for (; i < validateSource.length; i++) {
|
|
if (validateSource[i] === '{') depth++;
|
|
else if (validateSource[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
// Extract just the body content (between the braces)
|
|
const bodyContent = validateSource.slice(braceOpen + 1, i);
|
|
|
|
// Emit as a standalone named function so verify.cjs can require() and call it.
|
|
return `function phaseVariants(phase) {\n${bodyContent}\n}`;
|
|
}
|
|
|
|
export async function buildValidateCjs() {
|
|
const distUrl = new URL('../dist/query/validate.js', import.meta.url);
|
|
const validateSource = await readFile(fileURLToPath(distUrl), 'utf-8');
|
|
|
|
const phaseVariantsBody = extractPhaseVariantsBody(validateSource);
|
|
|
|
// buildRoadmapPhaseVariants: parse ROADMAP.md and return {roadmapPhases, roadmapPhaseVariants}
|
|
// roadmapPhases — raw phase tokens as written in headings (used for W006 check)
|
|
// roadmapPhaseVariants — all normalized variants of each roadmap phase (used for W007 check)
|
|
const buildRoadmapPhaseVariantsBody = `function buildRoadmapPhaseVariants(roadmapContent) {
|
|
const roadmapPhases = new Set();
|
|
const roadmapPhaseVariants = new Set();
|
|
const phasePattern = /#{2,4}\\s*Phase\\s+(\\d+[A-Z]?(?:\\.\\d+)*)\\s*:/gi;
|
|
let m;
|
|
while ((m = phasePattern.exec(roadmapContent)) !== null) {
|
|
roadmapPhases.add(m[1]);
|
|
for (const variant of phaseVariants(m[1])) roadmapPhaseVariants.add(variant);
|
|
}
|
|
return { roadmapPhases, roadmapPhaseVariants };
|
|
}`;
|
|
|
|
// buildNotStartedPhaseVariants: parse ROADMAP.md unchecked entries and return
|
|
// a Set of all variants of each unchecked phase (used for W006 skip logic).
|
|
const buildNotStartedPhaseVariantsBody = `function buildNotStartedPhaseVariants(roadmapContent) {
|
|
const notStartedPhases = new Set();
|
|
const uncheckedPattern = /-\\s*\\[\\s\\]\\s*\\*{0,2}Phase\\s+(\\d+[A-Z]?(?:\\.\\d+)*)[:\\s*]/gi;
|
|
let um;
|
|
while ((um = uncheckedPattern.exec(roadmapContent)) !== null) {
|
|
for (const variant of phaseVariants(um[1])) notStartedPhases.add(variant);
|
|
}
|
|
return notStartedPhases;
|
|
}`;
|
|
|
|
const parts = [
|
|
BANNER.trimEnd(),
|
|
'',
|
|
phaseVariantsBody,
|
|
'',
|
|
buildRoadmapPhaseVariantsBody,
|
|
'',
|
|
buildNotStartedPhaseVariantsBody,
|
|
'',
|
|
`module.exports = {
|
|
phaseVariants,
|
|
buildRoadmapPhaseVariants,
|
|
buildNotStartedPhaseVariants,
|
|
};`,
|
|
'',
|
|
];
|
|
|
|
return parts.join('\n');
|
|
}
|
|
|
|
async function main() {
|
|
const content = await buildValidateCjs();
|
|
const outPath = fileURLToPath(
|
|
new URL('../../get-shit-done/bin/lib/validate.generated.cjs', import.meta.url),
|
|
);
|
|
await writeFile(outPath, content, 'utf-8');
|
|
console.log(`Written: ${outPath}`);
|
|
}
|
|
|
|
// Only run main() when this file is the entry point.
|
|
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|
|
}
|