* test(26): reproduce W005/W006-archived/I001 false positives in CJS validate path Issue #26 (open-gsd/get-shit-done-redux): three validation drift items from PR #3479 were hand-ported to verify.cjs via PR #3806 but never routed through the generator pattern. This means they can drift again whenever validate.ts changes. RED tests assert that validate.generated.cjs exports four new items: - phaseDirNameRe (W005 regex — /^\d{2,}(?:\.\d+)*-[\w-]+$/) - MILESTONE_ARCHIVE_DIR_RE (W006-archived — /^v\d+.*-phases$/i) - PHASE_TOKEN_FROM_DIR_RE (W006-archived — phase dir token extractor) - canonicalPlanStem (I001 — plan/summary stem canonicalization) Three of four new export tests are RED (exports missing from generated artifact). Behavioral tests (W005 no-false-positive, W006-archived no-false-positive, I001 no-false-positive) are GREEN because #3806's hand-ported fixes are present. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6), PR #3479 (original fix), PR #3806 (hand-port). * chore(26): extend gen-validate.mjs to export W005/W006-archived/I001 helpers Issue #26 (open-gsd/get-shit-done-redux): extend gen-validate.mjs (introduced in PR #156 / issue #6) to also extract the three drift items that PR #3806 hand-ported to verify.cjs but were never routed through the generator. New helpers added to gen-validate.mjs: phaseDirNameRe (PHASE_DIR_NAME_RE) — W005 phase directory naming regex. /^\d{2,}(?:\.\d+)*-[\w-]+$/ accepts multi-digit prefixes (999.1-foo valid). Requires adding PHASE_DIR_NAME_RE as a named constant to validate.ts so it appears as an extractable identifier in the compiled output. PHASE_TOKEN_FROM_DIR_RE — W006-archived regex; extracts phase token from directory names like "64-auth-service" → "64". Used by forEachArchivedPhaseToken() and collectDiskPhases() in verify.cjs. MILESTONE_ARCHIVE_DIR_RE — W006-archived regex; matches milestone archive directory names like "v1.0-phases". Used by listMilestoneArchiveDirs(). canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization. '68-01-scaffolding' → '68-01'. Top-level named function in compiled output. Extraction approach: PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE are module-level const assignments, extracted via extractConstRegExp() (handles both `const` and `export const` prefixes). PHASE_DIR_NAME_RE is the new named export added to validate.ts in this commit. canonicalPlanStem is a top-level function, extracted via extractTopLevelFunction() (brace-balanced). validate.ts change: inline regex in Check 6 extracted to named constant PHASE_DIR_NAME_RE (exported) and Check 6 updated to reference it. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6). * chore(26): regenerate validate.generated.cjs with W005/W006-archived/I001 helpers Re-run of sdk/scripts/gen-validate.mjs after extending it in the preceding commit. The artifact now exports seven items (was three): New (issue #26): phaseDirNameRe — /^\d{2,}(?:\.\d+)*-[\w-]+$/ (W005 check) PHASE_TOKEN_FROM_DIR_RE — phase token extractor regex (W006-archived) MILESTONE_ARCHIVE_DIR_RE — archive dir name matcher (W006-archived) canonicalPlanStem() — PLAN/SUMMARY stem canonicalization (I001) Existing (issue #6): phaseVariants() buildRoadmapPhaseVariants() buildNotStartedPhaseVariants() Freshness check: node sdk/scripts/check-validate-fresh.mjs → "fresh". References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6). * fix(26): migrate verify.cjs W005/W006-archived/I001 call sites to generated helpers Issue #26 (open-gsd/get-shit-done-redux): three hand-maintained items in verify.cjs now consumed from validate.generated.cjs (ADR-3524 §4 adapter pattern). Changes: - Top-of-file require(): extend to also destructure phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem from validate.generated.cjs (issue #26 exports). - Remove inline PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE constants (lines ~403-404). Now sourced from generated artifact. listMilestoneArchiveDirs and forEachArchivedPhaseToken pick them up via the require() at top of file. - Check 6 (W005): replace inline regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ with phaseDirNameRe from validate.generated.cjs. No behavior change. - Remove inline canonicalPlanStem() function (~8 lines). Now sourced from validate.generated.cjs. Check 7 (I001) continues to call it as before. Public API of verify.cjs unchanged. Same migration shape as PR #156's Check 8. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6), PR #3479 (original fix), PR #3806 (hand-port that #26 supersedes). * docs(26): extend ADR-3524 2026-05-23 amendment with #26 scope Extends the existing 2026-05-23 amendment (not a new dated section) to document the W005/W006-archived/I001 generator migration introduced by issue #26. Key points documented: - Four new exports added to validate.generated.cjs (phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem) - W006-archived coverage note: both fixes were already in verify.cjs from #3806; the gap was generator coverage of the regex constants - Extraction methods: extractConstRegExp() and extractTopLevelFunction() - Parity tests: tests/26-w005-w006-i001-cjs-drift-regression.test.cjs (7 tests) - Cross-reference: issue #26 completes the validate.ts ↔ verify.cjs migration scope started by issue #6 References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6), PR #3479 (original fix), PR #3806 (hand-port). * chore(26): add changeset fragment for W005/W006-archived/I001 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. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
289 lines
12 KiB
JavaScript
289 lines
12 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 helpers and constants, then emits
|
|
* get-shit-done/bin/lib/validate.generated.cjs.
|
|
*
|
|
* The generated module exports seven items (three from issue #6, four from #26):
|
|
*
|
|
* Issue #6 drift items:
|
|
* 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).
|
|
*
|
|
* Issue #26 drift items:
|
|
* 4. phaseDirNameRe (PHASE_DIR_NAME_RE) — regex constant for W005 phase
|
|
* directory naming check. /^\d{2,}(?:\.\d+)*-[\w-]+$/ accepts multi-digit
|
|
* prefixes. verify.cjs Check 6 previously had an inline copy.
|
|
*
|
|
* 5. PHASE_TOKEN_FROM_DIR_RE — regex constant used by forEachArchivedPhaseToken()
|
|
* to extract the phase token from a directory name. verify.cjs had an inline copy.
|
|
*
|
|
* 6. MILESTONE_ARCHIVE_DIR_RE — regex constant used to identify milestone archive
|
|
* directories under .planning/milestones/. verify.cjs had an inline copy.
|
|
*
|
|
* 7. canonicalPlanStem(stem) — converts a PLAN file stem to its canonical form for
|
|
* PLAN/SUMMARY matching (I001 check). '68-01-scaffolding' → '68-01'.
|
|
* verify.cjs Check 7 previously had an inline copy.
|
|
*
|
|
* Extraction approach: phaseVariants is a closure inside validateHealth (not a module
|
|
* export), extracted via brace-balanced source-text parsing. Named constants and top-level
|
|
* functions (PHASE_DIR_NAME_RE, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE,
|
|
* canonicalPlanStem) are extracted by simple line-scanning from the compiled source.
|
|
*
|
|
* 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)
|
|
* - Issue #26 (open-gsd/get-shit-done-redux) — #26 extends issue #6's generator
|
|
* - PR #154 (issue #4) — generator pattern precedent
|
|
* - PR #156 (issue #6) — validate.ts generator that #26 extends
|
|
*/
|
|
|
|
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 and regex constants extracted from
|
|
* sdk/src/query/validate.ts. No I/O. No async. No filesystem operations.
|
|
*
|
|
* Issue #6 drift items (three helpers):
|
|
* 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.
|
|
*
|
|
* Issue #26 drift items (four constants/helpers):
|
|
* 4. phaseDirNameRe — W005 phase directory naming regex (was inline in verify.cjs Check 6).
|
|
* 5. PHASE_TOKEN_FROM_DIR_RE — extracts phase token from dir name (was inline in
|
|
* verify.cjs forEachArchivedPhaseToken / collectDiskPhases).
|
|
* 6. MILESTONE_ARCHIVE_DIR_RE — identifies milestone archive directories (was inline).
|
|
* 7. canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization (was inline in Check 7).
|
|
*
|
|
* 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)
|
|
* - Issue #26 (open-gsd/get-shit-done-redux)
|
|
* - PR #154 (issue #4) — generator pattern precedent
|
|
* - PR #156 (issue #6) — validate.ts generator that #26 extends
|
|
*/
|
|
|
|
`;
|
|
|
|
/**
|
|
* 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}`;
|
|
}
|
|
|
|
/**
|
|
* Extract a top-level const RegExp assignment from the source.
|
|
*
|
|
* Looks for the line `const <name> = /<pattern>/<flags>;` and returns the full
|
|
* assignment statement as a `module.exports`-compatible const declaration
|
|
* (renaming to the export name when it differs from the source name).
|
|
*
|
|
* @param {string} source - Compiled JS source text
|
|
* @param {string} sourceName - The const name as it appears in the compiled output
|
|
* @param {string} [exportName] - The name to export under (defaults to sourceName)
|
|
*/
|
|
function extractConstRegExp(source, sourceName, exportName) {
|
|
const nameToUse = exportName ?? sourceName;
|
|
const lines = source.split('\n');
|
|
// Match both `const <name> = ...` and `export const <name> = ...`
|
|
const suffix = `const ${sourceName} = `;
|
|
const line = lines.find((l) => l === suffix.trimStart() + l.slice(suffix.trimStart().length)
|
|
|| l.startsWith(suffix) || l.startsWith(`export ${suffix}`));
|
|
// Simpler: find a line that contains `const <name> = ` (anywhere after optional export)
|
|
const matchLine = lines.find((l) => {
|
|
const trimmed = l.replace(/^export\s+/, '');
|
|
return trimmed.startsWith(`const ${sourceName} = `);
|
|
});
|
|
if (!matchLine) throw new Error(`Could not find "const ${sourceName} = ..." in compiled validate.js`);
|
|
// Extract just the value (after `const <name> = `)
|
|
const assignIdx = matchLine.indexOf(`const ${sourceName} = `);
|
|
const valueStart = assignIdx + `const ${sourceName} = `.length;
|
|
const value = matchLine.slice(valueStart).replace(/;$/, '').trim();
|
|
return `const ${nameToUse} = ${value};`;
|
|
}
|
|
|
|
/**
|
|
* Extract a top-level named function declaration from the source.
|
|
*
|
|
* Matches `function <name>(<params>) {` and extracts the complete function body
|
|
* using a brace-balanced parser.
|
|
*
|
|
* @param {string} source - Compiled JS source text
|
|
* @param {string} name - The function name as it appears in the compiled output
|
|
*/
|
|
function extractTopLevelFunction(source, name) {
|
|
// Match a top-level function declaration (not prefixed by spaces/async/export)
|
|
const marker = `function ${name}(`;
|
|
const start = source.indexOf(marker);
|
|
if (start === -1) throw new Error(`Could not find top-level function "${name}" in compiled validate.js`);
|
|
|
|
const braceOpen = source.indexOf('{', start);
|
|
let depth = 0;
|
|
let i = braceOpen;
|
|
for (; i < source.length; i++) {
|
|
if (source[i] === '{') depth++;
|
|
else if (source[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
return source.slice(start, i + 1);
|
|
}
|
|
|
|
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);
|
|
|
|
// Issue #26: extract regex constants and canonicalPlanStem from compiled output.
|
|
//
|
|
// phaseDirNameRe — the PHASE_DIR_NAME_RE constant added to validate.ts for W005.
|
|
// Named 'phaseDirNameRe' in the export (camelCase for JS convention).
|
|
const phaseDirNameReLine = extractConstRegExp(validateSource, 'PHASE_DIR_NAME_RE', 'phaseDirNameRe');
|
|
|
|
// PHASE_TOKEN_FROM_DIR_RE — extracts phase token from a directory name like "64-auth-service".
|
|
// Exported under its original name for direct use in verify.cjs.
|
|
const phaseTokenFromDirReLine = extractConstRegExp(validateSource, 'PHASE_TOKEN_FROM_DIR_RE');
|
|
|
|
// MILESTONE_ARCHIVE_DIR_RE — matches milestone archive dir names like "v1.0-phases".
|
|
const milestoneArchiveDirReLine = extractConstRegExp(validateSource, 'MILESTONE_ARCHIVE_DIR_RE');
|
|
|
|
// canonicalPlanStem(stem) — I001 PLAN/SUMMARY stem canonicalization.
|
|
// '68-01-scaffolding' → '68-01'. Top-level named function in the compiled output.
|
|
const canonicalPlanStemBody = extractTopLevelFunction(validateSource, 'canonicalPlanStem');
|
|
|
|
// 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(),
|
|
'',
|
|
// Issue #26: regex constants extracted from compiled validate.js
|
|
'// ── Issue #26: regex constants (W005, W006-archived) ────────────────────────',
|
|
phaseDirNameReLine,
|
|
phaseTokenFromDirReLine,
|
|
milestoneArchiveDirReLine,
|
|
'',
|
|
// Issue #26: canonicalPlanStem (I001)
|
|
'// ── Issue #26: I001 canonicalization ────────────────────────────────────────',
|
|
canonicalPlanStemBody,
|
|
'',
|
|
// Issue #6: phaseVariants closure (W006/W007)
|
|
'// ── Issue #6: phase variant helpers (W006/W007) ──────────────────────────────',
|
|
phaseVariantsBody,
|
|
'',
|
|
buildRoadmapPhaseVariantsBody,
|
|
'',
|
|
buildNotStartedPhaseVariantsBody,
|
|
'',
|
|
`module.exports = {
|
|
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
|
|
phaseDirNameRe,
|
|
PHASE_TOKEN_FROM_DIR_RE,
|
|
MILESTONE_ARCHIVE_DIR_RE,
|
|
canonicalPlanStem,
|
|
// Issue #6 exports (W006/W007 phase variant helpers)
|
|
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);
|
|
});
|
|
}
|