build(#188): retire sdk generator and freshness scripts

This commit is contained in:
Tom Boucher
2026-05-25 09:41:37 -04:00
parent 140c3ed76d
commit 4c92aacca5
43 changed files with 1 additions and 5269 deletions

View File

@@ -1,83 +0,0 @@
/**
* Shared helpers for gen-*.mjs generator scripts.
*
* Provides requireFreshDist(), a pre-generation guard that verifies the
* compiled sdk/dist artifact is newer than its TypeScript source. If the
* dist file is missing or stale, the function exits 1 with a clear,
* actionable error message so the developer knows to run `npm run build:sdk`.
*
* Single source of truth — import from here rather than duplicating the mtime
* logic in each generator.
*
* @example
* import { requireFreshDist } from './_gen-helpers.mjs';
* requireFreshDist('sdk/dist/query/secrets.js', 'sdk/src/query/secrets.ts');
*/
import { statSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// Resolve the repo root from this file's location (sdk/scripts/ → repo root).
// GSD_REPO_ROOT env override allows tests to redirect to a temp directory so
// they can freely create/delete dist fixtures without mutating the real tree.
const REPO_ROOT = process.env.GSD_REPO_ROOT
? resolve(process.env.GSD_REPO_ROOT)
: resolve(fileURLToPath(import.meta.url), '..', '..', '..');
/**
* Assert that a compiled dist file is at least as new as its TypeScript source.
*
* Design notes:
* - Single Responsibility: each generator is responsible for verifying its
* own preconditions before touching dist. This keeps the guard co-located
* with the code that depends on it.
* - Composability: callers that have already built can call generators
* directly; the guard is a cheap mtime check, not a rebuild trigger.
* - Debuggability: the error message names both paths and both mtimes so the
* developer can see at a glance what is stale and what to do about it.
* - Performance: two synchronous statSync calls — effectively zero cost
* relative to the import/transform work the generator does next.
*
* NOT auto-building: auto-building inside the generator couples it to the
* build toolchain, bloats its dependency graph, and removes composability —
* a caller that already built cannot skip the redundant rebuild. The guard
* pattern is the correct separation: tell the developer what to do, not do it
* for them.
*
* @param {string} distPath - Repo-relative path to the compiled dist file,
* e.g. 'sdk/dist/query/secrets.js'
* @param {string} tsSourcePath - Repo-relative path to the TypeScript source
* file, e.g. 'sdk/src/query/secrets.ts'
*/
export function requireFreshDist(distPath, tsSourcePath) {
const distAbs = resolve(REPO_ROOT, distPath);
const tsAbs = resolve(REPO_ROOT, tsSourcePath);
if (!existsSync(distAbs)) {
console.error(
`ERROR: ${distPath} does not exist. Run \`npm run build:sdk\` first.`,
);
process.exit(1);
}
if (!existsSync(tsAbs)) {
console.error(
`ERROR: ${tsSourcePath} does not exist. Cannot verify dist freshness.`,
);
process.exit(1);
}
const distMtime = statSync(distAbs).mtimeMs;
const tsMtime = statSync(tsAbs).mtimeMs;
if (distMtime < tsMtime) {
console.error(
`ERROR: ${distPath} is stale relative to ${tsSourcePath} ` +
`(dist mtime ${new Date(distMtime).toISOString()}, ` +
`ts mtime ${new Date(tsMtime).toISOString()}). ` +
`Run \`npm run build:sdk\` first.`,
);
process.exit(1);
}
}

View File

@@ -1,105 +0,0 @@
#!/usr/bin/env node
import { createRequire } from 'node:module';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const here = dirname(fileURLToPath(import.meta.url));
const {
STATE_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.state.js');
const {
VERIFY_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.verify.js');
const {
INIT_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.init.js');
const {
PHASE_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.phase.js');
const {
PHASES_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.phases.js');
const {
VALIDATE_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.validate.js');
const {
ROADMAP_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.roadmap.js');
const {
STATE_COMMAND_ALIASES,
VERIFY_COMMAND_ALIASES,
INIT_COMMAND_ALIASES,
PHASE_COMMAND_ALIASES,
PHASES_COMMAND_ALIASES,
VALIDATE_COMMAND_ALIASES,
ROADMAP_COMMAND_ALIASES,
NON_FAMILY_COMMAND_ALIASES,
} = await import('../dist/query/command-aliases.generated.js');
const {
NON_FAMILY_COMMAND_MANIFEST,
} = await import('../dist/query/command-manifest.non-family.js');
const cjsAliases = require(resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs'));
function toAliasEntries(manifest, family) {
const prefix = `${family}.`;
return manifest.map((entry) => ({
canonical: entry.canonical,
aliases: [...entry.aliases],
subcommand: entry.canonical.slice(prefix.length),
mutation: entry.mutation,
}));
}
function toNonFamilyAliasEntries(manifest) {
return [...manifest]
.sort((a, b) => a.canonical.localeCompare(b.canonical))
.map((entry) => ({
canonical: entry.canonical,
aliases: [...entry.aliases],
mutation: entry.mutation,
}));
}
function assertEqual(label, actual, expected) {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
if (a !== e) {
throw new Error(
`${label} drift detected. Regenerate command alias artifacts and commit them.`,
);
}
}
const expectedState = toAliasEntries(STATE_COMMAND_MANIFEST, 'state');
const expectedVerify = toAliasEntries(VERIFY_COMMAND_MANIFEST, 'verify');
const expectedInit = toAliasEntries(INIT_COMMAND_MANIFEST, 'init');
const expectedPhase = toAliasEntries(PHASE_COMMAND_MANIFEST, 'phase');
const expectedPhases = toAliasEntries(PHASES_COMMAND_MANIFEST, 'phases');
const expectedValidate = toAliasEntries(VALIDATE_COMMAND_MANIFEST, 'validate');
const expectedRoadmap = toAliasEntries(ROADMAP_COMMAND_MANIFEST, 'roadmap');
const expectedNonFamily = toNonFamilyAliasEntries(NON_FAMILY_COMMAND_MANIFEST);
assertEqual('TS STATE_COMMAND_ALIASES', STATE_COMMAND_ALIASES, expectedState);
assertEqual('TS VERIFY_COMMAND_ALIASES', VERIFY_COMMAND_ALIASES, expectedVerify);
assertEqual('TS INIT_COMMAND_ALIASES', INIT_COMMAND_ALIASES, expectedInit);
assertEqual('TS PHASE_COMMAND_ALIASES', PHASE_COMMAND_ALIASES, expectedPhase);
assertEqual('TS PHASES_COMMAND_ALIASES', PHASES_COMMAND_ALIASES, expectedPhases);
assertEqual('TS VALIDATE_COMMAND_ALIASES', VALIDATE_COMMAND_ALIASES, expectedValidate);
assertEqual('TS ROADMAP_COMMAND_ALIASES', ROADMAP_COMMAND_ALIASES, expectedRoadmap);
assertEqual('TS NON_FAMILY_COMMAND_ALIASES', NON_FAMILY_COMMAND_ALIASES, expectedNonFamily);
assertEqual('CJS STATE_COMMAND_ALIASES', cjsAliases.STATE_COMMAND_ALIASES, expectedState);
assertEqual('CJS VERIFY_COMMAND_ALIASES', cjsAliases.VERIFY_COMMAND_ALIASES, expectedVerify);
assertEqual('CJS INIT_COMMAND_ALIASES', cjsAliases.INIT_COMMAND_ALIASES, expectedInit);
assertEqual('CJS PHASE_COMMAND_ALIASES', cjsAliases.PHASE_COMMAND_ALIASES, expectedPhase);
assertEqual('CJS PHASES_COMMAND_ALIASES', cjsAliases.PHASES_COMMAND_ALIASES, expectedPhases);
assertEqual('CJS VALIDATE_COMMAND_ALIASES', cjsAliases.VALIDATE_COMMAND_ALIASES, expectedValidate);
assertEqual('CJS ROADMAP_COMMAND_ALIASES', cjsAliases.ROADMAP_COMMAND_ALIASES, expectedRoadmap);
assertEqual('CJS NON_FAMILY_COMMAND_ALIASES', cjsAliases.NON_FAMILY_COMMAND_ALIASES, expectedNonFamily);
console.log('command alias artifacts are fresh');

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for get-shit-done/bin/lib/configuration.generated.cjs.
*
* Re-runs the generator in-memory, compares to the committed file,
* exits 0 if equal, 1 if not.
*
* Usage: node sdk/scripts/check-configuration-fresh.mjs
* Or: cd sdk && npm run check:configuration-fresh
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve, dirname } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, '..', '..');
const { buildConfigurationCjs } = await import('./gen-configuration.mjs');
const expected = buildConfigurationCjs();
const committedPath = resolve(repoRoot, 'get-shit-done', 'bin', 'lib', 'configuration.generated.cjs');
let committed;
try {
committed = readFileSync(committedPath, 'utf-8');
} catch (err) {
console.error(`configuration.generated.cjs not found at ${committedPath}`);
console.error('Run: cd sdk && npm run gen:configuration');
process.exit(1);
}
if (committed === expected) {
console.log('configuration.generated.cjs is fresh');
process.exit(0);
} else {
console.error('configuration.generated.cjs is STALE. Regenerate with:');
console.error(' cd sdk && npm run gen:configuration');
process.exit(1);
}

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for decisions.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Run: node sdk/scripts/check-decisions-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildDecisionsCjs } from './gen-decisions.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const expected = await buildDecisionsCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'decisions.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('decisions.generated.cjs is fresh');
process.exit(0);
} else {
console.error('decisions.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:decisions');
process.exit(1);
}

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for phase.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Uses the same pattern as check-project-root-fresh.mjs: imports buildPhaseCjs()
* from the generator directly rather than duplicating the build logic.
*
* Run: node sdk/scripts/check-phase-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
// Import the generator function directly (avoids duplicating logic).
const { buildPhaseCjs } = await import('./gen-phase.mjs');
const expected = await buildPhaseCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'phase.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('phase.generated.cjs is fresh');
process.exit(0);
} else {
console.error('phase.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:phase');
process.exit(1);
}

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for phase-lifecycle.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Uses the same pattern as check-project-root-fresh.mjs: imports
* buildPhaseLifecycleCjs() from the generator directly.
*
* Unlike check-phase-fresh.mjs and check-phase-lifecycle-policy-fresh.mjs,
* this generator does NOT require sdk/dist (it defines the pure functions
* directly in the generator module rather than importing compiled output).
*
* Run: node sdk/scripts/check-phase-lifecycle-fresh.mjs
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
// Import the generator function directly (avoids duplicating logic).
const { buildPhaseLifecycleCjs } = await import('./gen-phase-lifecycle.mjs');
const expected = await buildPhaseLifecycleCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'phase-lifecycle.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('phase-lifecycle.generated.cjs is fresh');
process.exit(0);
} else {
console.error('phase-lifecycle.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:phase-lifecycle');
process.exit(1);
}

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for phase-lifecycle-policy.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Uses the same pattern as check-project-root-fresh.mjs: imports
* buildPhaseLifecyclePolicyCjs() from the generator directly.
*
* Run: node sdk/scripts/check-phase-lifecycle-policy-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
// Import the generator function directly (avoids duplicating logic).
const { buildPhaseLifecyclePolicyCjs } = await import('./gen-phase-lifecycle-policy.mjs');
const expected = await buildPhaseLifecyclePolicyCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'phase-lifecycle-policy.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('phase-lifecycle-policy.generated.cjs is fresh');
process.exit(0);
} else {
console.error('phase-lifecycle-policy.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:phase-lifecycle-policy');
process.exit(1);
}

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for plan-scan.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Run: node sdk/scripts/check-plan-scan-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildPlanScanCjs } from './gen-plan-scan.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const expected = await buildPlanScanCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'plan-scan.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('plan-scan.generated.cjs is fresh');
process.exit(0);
} else {
console.error('plan-scan.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:plan-scan');
process.exit(1);
}

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for project-root.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Uses Phase 3's cleaner pattern: imports buildProjectRootCjs() from the
* generator directly rather than duplicating the build logic.
*
* Run: node sdk/scripts/check-project-root-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
// Import the generator function directly (avoids duplicating logic).
const { buildProjectRootCjs } = await import('./gen-project-root.mjs');
const expected = await buildProjectRootCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'project-root.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('project-root.generated.cjs is fresh');
process.exit(0);
} else {
console.error('project-root.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:project-root');
process.exit(1);
}

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for schema-detect.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Run: node sdk/scripts/check-schema-detect-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildSchemaDetectCjs } from './gen-schema-detect.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const expected = await buildSchemaDetectCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'schema-detect.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('schema-detect.generated.cjs is fresh');
process.exit(0);
} else {
console.error('schema-detect.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:schema-detect');
process.exit(1);
}

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for secrets.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Run: node sdk/scripts/check-secrets-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildSecretsCjs } from './gen-secrets.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const expected = await buildSecretsCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'secrets.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('secrets.generated.cjs is fresh');
process.exit(0);
} else {
console.error('secrets.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:secrets');
process.exit(1);
}

View File

@@ -1,114 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for state-document.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Run: node sdk/scripts/check-state-document-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/state/index.ts
* Regenerate: cd sdk && npm run gen:state-document
*
* STATE.md Document Module — pure transforms for STATE.md text.
* This module does not read the filesystem and does not own persistence or locking.
*/`;
/**
* Extract a top-level function declaration (non-exported) from a JS source
* string by scanning for `function <name>(` and capturing the entire body
* including balanced braces.
*/
function extractFunctionFromSource(source, name) {
const marker = `function ${name}(`;
const start = source.indexOf(marker);
if (start === -1) throw new Error(`Could not find function ${name} in compiled source`);
const braceOpen = source.indexOf('{', start);
if (braceOpen === -1) throw new Error(`Could not find opening brace for function ${name}`);
let depth = 0;
let i = braceOpen;
for (; i < source.length; i++) {
if (source[i] === '{') depth++;
else if (source[i] === '}') { depth--; if (depth === 0) break; }
}
if (depth !== 0) {
throw new Error(`Could not find closing brace for function ${name}`);
}
return source.slice(start, i + 1);
}
const distUrl = new URL('../dist/state/index.js', import.meta.url);
const {
stateExtractField,
stateReplaceField,
stateReplaceFieldWithFallback,
normalizeStateStatus,
computeProgressPercent,
shouldPreserveExistingProgress,
normalizeProgressNumbers,
} = await import(distUrl.href);
const compiledSource = await readFile(fileURLToPath(distUrl), 'utf-8');
const escapeRegexBody = extractFunctionFromSource(compiledSource, 'escapeRegex');
const toFiniteNumberBody = extractFunctionFromSource(compiledSource, 'toFiniteNumber');
const existingProgressExceedsDerivedBody = extractFunctionFromSource(compiledSource, 'existingProgressExceedsDerived');
const stateExtractFieldBody = stateExtractField.toString();
const stateReplaceFieldBody = stateReplaceField.toString();
const stateReplaceFieldWithFallbackBody = stateReplaceFieldWithFallback.toString();
const normalizeStateStatusBody = normalizeStateStatus.toString();
const computeProgressPercentBody = computeProgressPercent.toString();
const shouldPreserveExistingProgressBody = shouldPreserveExistingProgress.toString();
const normalizeProgressNumbersBody = normalizeProgressNumbers.toString();
const expected = [
BANNER,
'',
'// Internal helpers',
escapeRegexBody,
'',
toFiniteNumberBody,
'',
existingProgressExceedsDerivedBody,
'',
stateExtractFieldBody,
'',
stateReplaceFieldBody,
'',
stateReplaceFieldWithFallbackBody,
'',
normalizeStateStatusBody,
'',
computeProgressPercentBody,
'',
shouldPreserveExistingProgressBody,
'',
normalizeProgressNumbersBody,
'',
'module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };',
'',
].join('\n');
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'state-document.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('state-document.generated.cjs is fresh');
process.exit(0);
} else {
console.error('state-document.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:state-document');
process.exit(1);
}

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for validate.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Uses the same pattern as check-phase-lifecycle-policy-fresh.mjs: imports
* buildValidateCjs() from the generator directly.
*
* Run: node sdk/scripts/check-validate-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in 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
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
// Import the generator function directly (avoids duplicating logic).
const { buildValidateCjs } = await import('./gen-validate.mjs');
const expected = await buildValidateCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'validate.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('validate.generated.cjs is fresh');
process.exit(0);
} else {
console.error('validate.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:validate');
process.exit(1);
}

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for workstream-inventory-builder.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Run: node sdk/scripts/check-workstream-inventory-builder-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildWorkstreamInventoryBuilderCjs } from './gen-workstream-inventory-builder.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const expected = await buildWorkstreamInventoryBuilderCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'workstream-inventory-builder.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('workstream-inventory-builder.generated.cjs is fresh');
process.exit(0);
} else {
console.error('workstream-inventory-builder.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:workstream-inventory-builder');
process.exit(1);
}

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env node
/**
* Freshness check for workstream-name-policy.generated.cjs.
*
* Regenerates the expected CJS content in-memory (without writing to disk) and
* compares it to the committed file. Exits 0 if they match, 1 if stale.
*
* Run: node sdk/scripts/check-workstream-name-policy-fresh.mjs
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildWorkstreamNamePolicyCjs } from './gen-workstream-name-policy.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const expected = await buildWorkstreamNamePolicyCjs();
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'workstream-name-policy.generated.cjs');
const committed = await readFile(committedPath, 'utf-8');
if (expected === committed) {
console.log('workstream-name-policy.generated.cjs is fresh');
process.exit(0);
} else {
console.error('workstream-name-policy.generated.cjs is STALE.');
console.error('Regenerate: cd sdk && npm run gen:workstream-name-policy');
process.exit(1);
}

View File

@@ -1,196 +0,0 @@
#!/usr/bin/env node
/**
* Build-time alias generator skeleton for command-manifest-driven routing.
*
* This pilot commits generated artifacts directly; this script documents and
* preserves the generation seam so future command families can be migrated
* without hand-maintained alias duplication.
*/
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { COMMAND_DEFINITIONS_BY_FAMILY } from '../src/query/command-definition.js';
import { NON_FAMILY_COMMAND_MANIFEST } from '../src/query/command-manifest.non-family.js';
function toSubcommand(canonical: string, family: 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap'): string {
const prefix = `${family}.`;
return canonical.startsWith(prefix) ? canonical.slice(prefix.length) : canonical;
}
async function main(): Promise<void> {
const stateEntries = COMMAND_DEFINITIONS_BY_FAMILY.state.map((entry) => ({
canonical: entry.canonical,
aliases: entry.aliases,
subcommand: toSubcommand(entry.canonical, 'state'),
mutation: entry.mutation,
}));
const verifyEntries = COMMAND_DEFINITIONS_BY_FAMILY.verify.map((entry) => ({
canonical: entry.canonical,
aliases: entry.aliases,
subcommand: toSubcommand(entry.canonical, 'verify'),
mutation: entry.mutation,
}));
const initEntries = COMMAND_DEFINITIONS_BY_FAMILY.init.map((entry) => ({
canonical: entry.canonical,
aliases: entry.aliases,
subcommand: toSubcommand(entry.canonical, 'init'),
mutation: entry.mutation,
}));
const phaseEntries = COMMAND_DEFINITIONS_BY_FAMILY.phase.map((entry) => ({
canonical: entry.canonical,
aliases: entry.aliases,
subcommand: toSubcommand(entry.canonical, 'phase'),
mutation: entry.mutation,
}));
const phasesEntries = COMMAND_DEFINITIONS_BY_FAMILY.phases.map((entry) => ({
canonical: entry.canonical,
aliases: entry.aliases,
subcommand: toSubcommand(entry.canonical, 'phases'),
mutation: entry.mutation,
}));
const validateEntries = COMMAND_DEFINITIONS_BY_FAMILY.validate.map((entry) => ({
canonical: entry.canonical,
aliases: entry.aliases,
subcommand: toSubcommand(entry.canonical, 'validate'),
mutation: entry.mutation,
}));
const roadmapEntries = COMMAND_DEFINITIONS_BY_FAMILY.roadmap.map((entry) => ({
canonical: entry.canonical,
aliases: entry.aliases,
subcommand: toSubcommand(entry.canonical, 'roadmap'),
mutation: entry.mutation,
}));
// Non-family entries — sorted by canonical for deterministic output.
const nonFamilyEntries = [...NON_FAMILY_COMMAND_MANIFEST]
.sort((a, b) => a.canonical.localeCompare(b.canonical))
.map((entry) => ({
canonical: entry.canonical,
aliases: [...entry.aliases],
mutation: entry.mutation,
}));
// Serialise a FamilyCommandAlias entry as a single-line TS literal.
function serializeFamily(e: { canonical: string; aliases: string[]; subcommand: string; mutation: boolean }): string {
const aliases = `[${e.aliases.map((a) => `'${a}'`).join(', ')}]`;
return `{ canonical: '${e.canonical}', aliases: ${aliases}, subcommand: '${e.subcommand}', mutation: ${e.mutation} }`;
}
// Serialise a NonFamilyCommandAlias entry as a single-line TS literal.
function serializeNonFamily(e: { canonical: string; aliases: string[]; mutation: boolean }): string {
const aliases = `[${e.aliases.map((a) => `'${a}'`).join(', ')}]`;
return `{ canonical: '${e.canonical}', aliases: ${aliases}, mutation: ${e.mutation} }`;
}
function renderFamilyArray(entries: { canonical: string; aliases: string[]; subcommand: string; mutation: boolean }[]): string {
return `[\n${entries.map((e) => ` ${serializeFamily(e)},`).join('\n')}\n]`;
}
function renderNonFamilyArray(entries: { canonical: string; aliases: string[]; mutation: boolean }[]): string {
return `[\n${entries.map((e) => ` ${serializeNonFamily(e)},`).join('\n')}\n]`;
}
const tsOutPath = fileURLToPath(new URL('../src/query/command-aliases.generated.ts', import.meta.url));
const tsHeader = `/**\n * GENERATED FILE — command alias expansion for state.*, verify.*, init.*, phase.*, phases.*, validate.*, roadmap.*, and non-family commands.\n * Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap,non-family}.ts\n */\n\n`;
const tsBody = [
'export interface FamilyCommandAlias {',
' canonical: string;',
' aliases: string[];',
' subcommand: string;',
' mutation: boolean;',
'}',
'',
`export const STATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(stateEntries)} as const;`,
'',
`export const VERIFY_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(verifyEntries)} as const;`,
'',
`export const INIT_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(initEntries)} as const;`,
'',
`export const PHASE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(phaseEntries)} as const;`,
'',
`export const PHASES_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(phasesEntries)} as const;`,
'',
`export const VALIDATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(validateEntries)} as const;`,
'',
`export const ROADMAP_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(roadmapEntries)} as const;`,
'',
'export interface NonFamilyCommandAlias {',
' canonical: string;',
' aliases: string[];',
' mutation: boolean;',
'}',
'',
`export const NON_FAMILY_COMMAND_ALIASES: readonly NonFamilyCommandAlias[] = ${renderNonFamilyArray(nonFamilyEntries)} as const;`,
'',
'export const STATE_SUBCOMMANDS = new Set<string>(STATE_COMMAND_ALIASES.map((entry) => entry.subcommand));',
'export const VERIFY_SUBCOMMANDS = new Set<string>(VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand));',
'export const INIT_SUBCOMMANDS = new Set<string>(INIT_COMMAND_ALIASES.map((entry) => entry.subcommand));',
'export const PHASE_SUBCOMMANDS = new Set<string>(PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand));',
'export const PHASES_SUBCOMMANDS = new Set<string>(PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand));',
'export const VALIDATE_SUBCOMMANDS = new Set<string>(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand));',
'export const ROADMAP_SUBCOMMANDS = new Set<string>(ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand));',
'',
].join('\n');
await writeFile(tsOutPath, tsHeader + tsBody, 'utf-8');
// Also generate the CJS mirror used by get-shit-done/bin/lib/ seams.
// CJS is plain JavaScript — no type annotations.
const cjsOutPath = fileURLToPath(new URL('../../get-shit-done/bin/lib/command-aliases.generated.cjs', import.meta.url));
const cjsHeader = `'use strict';\n\n/**\n * GENERATED FILE — state.*, verify.*, init.*, phase.*, phases.*, validate.*, roadmap.*, and non-family alias/subcommand metadata for CJS routing.\n * Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap,non-family}.ts\n */\n\n`;
const cjsBody = [
`const STATE_COMMAND_ALIASES = ${JSON.stringify(stateEntries, null, 2)};`,
'',
`const VERIFY_COMMAND_ALIASES = ${JSON.stringify(verifyEntries, null, 2)};`,
'',
`const INIT_COMMAND_ALIASES = ${JSON.stringify(initEntries, null, 2)};`,
'',
`const PHASE_COMMAND_ALIASES = ${JSON.stringify(phaseEntries, null, 2)};`,
'',
`const PHASES_COMMAND_ALIASES = ${JSON.stringify(phasesEntries, null, 2)};`,
'',
`const VALIDATE_COMMAND_ALIASES = ${JSON.stringify(validateEntries, null, 2)};`,
'',
`const ROADMAP_COMMAND_ALIASES = ${JSON.stringify(roadmapEntries, null, 2)};`,
'',
`const NON_FAMILY_COMMAND_ALIASES = ${JSON.stringify(nonFamilyEntries, null, 2)};`,
'',
`const STATE_SUBCOMMANDS = STATE_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
`const VERIFY_SUBCOMMANDS = VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
`const INIT_SUBCOMMANDS = INIT_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
`const PHASE_SUBCOMMANDS = PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
`const PHASES_SUBCOMMANDS = PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
`const VALIDATE_SUBCOMMANDS = VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
`const ROADMAP_SUBCOMMANDS = ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
'',
`module.exports = {`,
` STATE_COMMAND_ALIASES,`,
` VERIFY_COMMAND_ALIASES,`,
` INIT_COMMAND_ALIASES,`,
` PHASE_COMMAND_ALIASES,`,
` PHASES_COMMAND_ALIASES,`,
` VALIDATE_COMMAND_ALIASES,`,
` ROADMAP_COMMAND_ALIASES,`,
` NON_FAMILY_COMMAND_ALIASES,`,
` STATE_SUBCOMMANDS,`,
` VERIFY_SUBCOMMANDS,`,
` INIT_SUBCOMMANDS,`,
` PHASE_SUBCOMMANDS,`,
` PHASES_SUBCOMMANDS,`,
` VALIDATE_SUBCOMMANDS,`,
` ROADMAP_SUBCOMMANDS,`,
`};`,
].join('\n');
await writeFile(cjsOutPath, cjsHeader + cjsBody, 'utf-8');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -1,180 +0,0 @@
#!/usr/bin/env node
/**
* Generator for get-shit-done/bin/lib/configuration.generated.cjs.
*
* Reads the compiled Configuration Module from sdk/dist/config/index.js
* and emits a CJS file that:
* 1. Requires the two JSON manifests from sdk/shared/
* 2. Exports loadConfig, normalizeLegacyKeys, mergeDefaults, migrateOnDisk,
* CONFIG_DEFAULTS, VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS
*
* Run via: cd sdk && npm run gen:configuration
* Or from repo root: node sdk/scripts/gen-configuration.mjs
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve, dirname } from 'node:path';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/config/index.js', 'sdk/src/config/index.ts');
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, '..', '..');
// ─── Read the compiled dist file for function extraction ─────────────────────
const distPath = resolve(here, '..', 'dist', 'config', 'index.js');
const distSrc = readFileSync(distPath, 'utf-8');
/**
* Extract a named function from the compiled dist source by scanning for
* `function <name>(` (or `async function <name>(`) and capturing the balanced
* braces body. Returns the full `[async] function name(...) { ... }` string,
* preserving the async keyword when present.
*/
function extractFunction(src, name) {
// Try async first, then plain function
let start = src.indexOf(`async function ${name}(`);
if (start === -1) start = src.indexOf(`function ${name}(`);
if (start === -1) throw new Error(`Function "${name}" not found in dist source`);
// Find opening brace
const braceStart = src.indexOf('{', start);
if (braceStart === -1) throw new Error(`No opening brace for "${name}"`);
// Balance braces
let depth = 0;
let i = braceStart;
while (i < src.length) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) {
return src.slice(start, i + 1);
}
}
i++;
}
throw new Error(`Unbalanced braces for "${name}"`);
}
const fnPlanningDir = extractFunction(distSrc, 'planningDir');
const fnDetectSubRepos = extractFunction(distSrc, 'detectSubRepos');
const fnDeepMergeConfig = extractFunction(distSrc, 'deepMergeConfig');
const fnNormalizeLegacyKeys = extractFunction(distSrc, 'normalizeLegacyKeys');
const fnMergeDefaults = extractFunction(distSrc, 'mergeDefaults');
const fnLoadConfig = extractFunction(distSrc, 'loadConfig');
const fnMigrateOnDisk = extractFunction(distSrc, 'migrateOnDisk');
// Capture DEPTH_TO_GRANULARITY constant
const dtgMatch = distSrc.match(/const DEPTH_TO_GRANULARITY = \{[^}]+\};/);
if (!dtgMatch) throw new Error('DEPTH_TO_GRANULARITY not found in dist source');
const dtgConst = dtgMatch[0];
// ─── Build CJS output ─────────────────────────────────────────────────────────
/**
* Build the CJS output string.
* Exported so check-configuration-fresh.mjs can call it without re-running the generator.
*/
export function buildConfigurationCjs() {
return [
`'use strict';`,
``,
`/**`,
` * GENERATED FILE — DO NOT EDIT.`,
` *`,
` * Source: sdk/src/config/index.ts`,
` * Regenerate: cd sdk && npm run gen:configuration`,
` *`,
` * Configuration Module — single source of truth for config loading,`,
` * legacy-key normalization, defaults merge, and explicit on-disk migration.`,
` */`,
``,
`const { readFileSync, writeFileSync, existsSync, readdirSync } = require('node:fs');`,
`const { join } = require('node:path');`,
``,
`// ─── Manifest requires ───────────────────────────────────────────────────────`,
`function loadConfigurationManifest(fileName) {`,
` const candidates = [`,
` // Installed runtime layout: get-shit-done/bin/shared/*.manifest.json`,
` join(__dirname, '..', 'shared', fileName),`,
` // Source-repo dev layout: sdk/shared/*.manifest.json`,
` join(__dirname, '..', '..', '..', 'sdk', 'shared', fileName),`,
` ];`,
` let lastErr = null;`,
` for (const candidate of candidates) {`,
` try {`,
` return require(candidate);`,
` } catch (err) {`,
` const isMissingCandidate =`,
` err && err.code === 'MODULE_NOT_FOUND' && String(err.message || '').includes(candidate);`,
` if (!isMissingCandidate) throw err;`,
` lastErr = err;`,
` }`,
` }`,
` throw new Error(`,
` \`\${fileName} not found. Tried:\\n\${candidates.map((p) => \` \${p}\`).join('\\n')}\\nLast error: \${lastErr?.message}\``,
` );`,
`}`,
``,
`const CONFIG_DEFAULTS = loadConfigurationManifest('config-defaults.manifest.json');`,
`const SCHEMA_MANIFEST = loadConfigurationManifest('config-schema.manifest.json');`,
`const VALID_CONFIG_KEYS = new Set(SCHEMA_MANIFEST.validKeys);`,
`const RUNTIME_STATE_KEYS = new Set(SCHEMA_MANIFEST.runtimeStateKeys);`,
`const DYNAMIC_KEY_PATTERNS = SCHEMA_MANIFEST.dynamicKeyPatterns.map((p) => {`,
` const pattern = new RegExp(p.source);`,
` return {`,
` ...p,`,
` test: (key) => {`,
` pattern.lastIndex = 0;`,
` return pattern.test(key);`,
` },`,
` };`,
`});`,
``,
`// ─── Depth → Granularity mapping ─────────────────────────────────────────────`,
dtgConst,
``,
`// ─── Internal helpers ─────────────────────────────────────────────────────────`,
fnPlanningDir,
``,
fnDetectSubRepos,
``,
fnDeepMergeConfig,
``,
`// ─── Exported functions ───────────────────────────────────────────────────────`,
fnNormalizeLegacyKeys,
``,
fnMergeDefaults,
``,
fnLoadConfig,
``,
fnMigrateOnDisk,
``,
`module.exports = {`,
` loadConfig,`,
` normalizeLegacyKeys,`,
` mergeDefaults,`,
` migrateOnDisk,`,
` CONFIG_DEFAULTS,`,
` VALID_CONFIG_KEYS,`,
` RUNTIME_STATE_KEYS,`,
` DYNAMIC_KEY_PATTERNS,`,
`};`,
``,
].join('\n');
}
// ─── Main: write output file (only when run directly) ────────────────────────
// Guard: don't write the file when imported by check-configuration-fresh.mjs.
// `process.argv[1]` is the absolute path of the entry-point script.
const _thisFile = fileURLToPath(import.meta.url);
if (process.argv[1] === _thisFile) {
const cjsOut = buildConfigurationCjs();
const outPath = resolve(repoRoot, 'get-shit-done', 'bin', 'lib', 'configuration.generated.cjs');
writeFileSync(outPath, cjsOut, 'utf-8');
console.log(`Generated: ${outPath}`);
}

View File

@@ -1,103 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Decisions CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/query/decisions.js,
* extracts the relevant function bodies via text transformation,
* then emits get-shit-done/bin/lib/decisions.generated.cjs.
*
* Source-of-truth: sdk/src/query/decisions.ts
*
* Run: cd sdk && npm run gen:decisions
* Freshness check: node sdk/scripts/check-decisions-fresh.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/query/decisions.js', 'sdk/src/query/decisions.ts');
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/decisions.ts
* Regenerate: cd sdk && npm run gen:decisions
*
* Shared parser for CONTEXT.md <decisions> blocks.
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
* Returns {id, text, category, tags, trackable} per decision.
* CJS callers that only use {id, text} safely ignore the extra fields.
*/
`;
export async function buildDecisionsCjs() {
// Read the compiled ESM source and transform to CJS.
// We extract only the pure logic (no Node.js imports, no query handler).
const distPath = fileURLToPath(new URL('../dist/query/decisions.js', import.meta.url));
const src = await readFile(distPath, 'utf-8');
// Strip the ESM-specific header lines (import statements, jsdoc at top)
// and the query handler (which uses Node async fs — not needed in CJS shim).
// We keep: DISCRETION_HEADINGS, NON_TRACKABLE_TAGS, stripFencedCode,
// extractDecisionsBlock, parseDecisions.
// Extract the module body between the imports and the query handler.
// Strategy: strip the leading imports and the trailing export const decisionsParse block.
let body = src;
// Remove leading import statements
body = body.replace(/^import\s+.*?;[\r\n]*/gm, '');
// Remove trailing query handler (from the `export const decisionsParse` line to end)
const handlerStart = body.indexOf('// ─── Query handler');
if (handlerStart !== -1) {
body = body.slice(0, handlerStart);
}
// Remove ESM export keywords (keep the function/const declarations)
body = body.replace(/^export (function|const) /gm, '$1 ');
// Remove source map comment
body = body.replace(/\/\/# sourceMappingURL=.*$/gm, '');
// Remove leading file-level jsdoc comment (keep module-level logic only)
body = body.replace(/^\/\*\*[\s\S]*?\*\/\n/m, '');
// Trim extra blank lines at start/end
body = body.trim();
const parts = [
BANNER.trimEnd(),
'',
body,
'',
'module.exports = { parseDecisions };',
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildDecisionsCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/decisions.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -1,185 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Phase Lifecycle Policy CJS artifact.
*
* Imports the compiled ESM output from sdk/dist/query/phase-lifecycle-policy.js,
* captures pure helper functions via Function.prototype.toString(), then emits
* get-shit-done/bin/lib/phase-lifecycle-policy.generated.cjs.
*
* All functions in phase-lifecycle-policy.ts are pure transforms — no I/O,
* no async, no filesystem operations. They are directly serializable via
* Function.prototype.toString(). The only external dependencies are:
* - GSDError / ErrorClassification (from ../errors.js) → replaced with a
* lightweight local throw-Error stub
* - escapeRegex (from ./helpers.js) → inlined from compiled source text
*
* This is the "I/O adapter pattern" from ADR-3524 Section 4 applied to pure
* helpers: pure logic goes into a Shared Module; the generated CJS artifact
* provides the same API surface with the SDK's error model swapped out for
* plain throws that CJS callers can catch and delegate to error().
*
* Run: cd sdk && npm run gen:phase-lifecycle-policy
* Check: node sdk/scripts/check-phase-lifecycle-policy-fresh.mjs
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
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/phase-lifecycle-policy.ts
* Regenerate: cd sdk && npm run gen:phase-lifecycle-policy
*
* Phase Lifecycle Policy — pure computation helpers for phase directory naming,
* roadmap entry generation, decimal-phase management, and ID computation.
* No I/O. No async. No filesystem operations.
*
* I/O adapter pattern (ADR-3524 §4): pure transforms extracted from the SDK;
* GSDError is replaced with plain throws that CJS callers can catch.
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
`;
/**
* Extract escapeRegex from compiled helpers.js source text.
* Used by collectDecimalSuffixesFromDirNames and collectDecimalSuffixesFromRoadmap.
*/
function extractEscapeRegexBody(helpersSource) {
const marker = 'function escapeRegex(';
const start = helpersSource.indexOf(marker);
if (start === -1) throw new Error('Could not find escapeRegex in compiled helpers.js');
const braceOpen = helpersSource.indexOf('{', start);
let depth = 0;
let i = braceOpen;
for (; i < helpersSource.length; i++) {
if (helpersSource[i] === '{') depth++;
else if (helpersSource[i] === '}') { depth--; if (depth === 0) break; }
}
return helpersSource.slice(start, i + 1);
}
export async function buildPhaseLifecyclePolicyCjs() {
const distUrl = new URL('../dist/query/phase-lifecycle-policy.js', import.meta.url);
const helpersUrl = new URL('../dist/query/helpers.js', import.meta.url);
const {
assertNoNullBytes,
assertSafePhaseDirName,
assertSafeProjectCode,
generatePhaseSlug,
parseMultiwordArg,
extractOneLinerFromBody,
scanSequentialMaxPhaseFromMilestone,
scanSequentialMaxPhaseFromDirs,
computeNextSequentialPhaseId,
computePhaseDirectory,
buildPhaseRoadmapEntry,
collectDecimalSuffixesFromDirNames,
collectDecimalSuffixesFromRoadmap,
computeNextDecimalPhase,
} = await import(distUrl.href);
const helpersSource = await readFile(fileURLToPath(helpersUrl), 'utf-8');
const escapeRegexBody = extractEscapeRegexBody(helpersSource);
// Stub for GSDError — throws plain Error so CJS callers can catch and delegate
// to error() (process.exit) as needed. The ErrorClassification enum values are
// unused at runtime in the generated artifact; they exist only for TypeScript typing.
const gsdErrorStub = `// Lightweight stub replacing sdk/src/errors.js GSDError.
// CJS callers that need to translate to process.exit(1) should catch these.
class GSDError extends Error {
constructor(message, classification) {
super(message);
this.name = 'GSDError';
this.classification = classification;
}
}
// ErrorClassification values used by policy functions
const ErrorClassification = { Validation: 'Validation', Internal: 'Internal' };`;
const parts = [
BANNER.trimEnd(),
'',
gsdErrorStub,
'',
'// escapeRegex — inlined from sdk/dist/query/helpers.js',
escapeRegexBody,
'',
assertNoNullBytes.toString(),
'',
assertSafePhaseDirName.toString(),
'',
assertSafeProjectCode.toString(),
'',
generatePhaseSlug.toString(),
'',
parseMultiwordArg.toString(),
'',
extractOneLinerFromBody.toString(),
'',
scanSequentialMaxPhaseFromMilestone.toString(),
'',
scanSequentialMaxPhaseFromDirs.toString(),
'',
computeNextSequentialPhaseId.toString(),
'',
computePhaseDirectory.toString(),
'',
buildPhaseRoadmapEntry.toString(),
'',
collectDecimalSuffixesFromDirNames.toString(),
'',
collectDecimalSuffixesFromRoadmap.toString(),
'',
computeNextDecimalPhase.toString(),
'',
`module.exports = {
GSDError,
assertNoNullBytes,
assertSafePhaseDirName,
assertSafeProjectCode,
generatePhaseSlug,
parseMultiwordArg,
extractOneLinerFromBody,
scanSequentialMaxPhaseFromMilestone,
scanSequentialMaxPhaseFromDirs,
computeNextSequentialPhaseId,
computePhaseDirectory,
buildPhaseRoadmapEntry,
collectDecimalSuffixesFromDirNames,
collectDecimalSuffixesFromRoadmap,
computeNextDecimalPhase,
};`,
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildPhaseLifecyclePolicyCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/phase-lifecycle-policy.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);
});
}

View File

@@ -1,187 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Phase Lifecycle CJS artifact.
*
* Emits get-shit-done/bin/lib/phase-lifecycle.generated.cjs which provides
* the pure-computation functions extracted from sdk/src/query/phase-lifecycle.ts.
*
* The phase-lifecycle.ts module contains mutation handlers (phaseAdd, phaseInsert,
* phaseRemove, phaseComplete) that are inherently async and I/O-bound. Per
* ADR-3524 Section 4: "I/O stays per-side." These mutation handlers are NOT generated.
*
* What IS generated: the pure-computation logic used by phaseComplete that was
* the root cause of issue #4:
* - deriveProgressFromRoadmap(roadmapContent): computes completed_phases,
* total_phases, total_plans from ROADMAP progress table content.
* This is the idempotency fix: deriving from ROADMAP instead of blind +1.
* - clampPercent(completed, total): percent computation with 100 ceiling.
*
* The CJS shim (bin/lib/phase.cjs) calls these pure functions with
* synchronously-read ROADMAP content. The SDK handler calls them with
* asynchronously-read content. Same logic, different I/O adapters.
*
* I/O adapter pattern from ADR-3524 Section 4 applied to mutation helpers:
* pure computation is shared; each side supplies its own I/O.
*
* Run: cd sdk && npm run gen:phase-lifecycle
* Check: node sdk/scripts/check-phase-lifecycle-fresh.mjs
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/phase-lifecycle.ts
* Regenerate: cd sdk && npm run gen:phase-lifecycle
*
* Phase Lifecycle Pure Helpers — pure-computation functions extracted from
* the phase-lifecycle SDK handler.
*
* I/O adapter pattern (ADR-3524 Section 4): each side supplies its own I/O
* (sync readFileSync for CJS, async readFile for SDK); the pure computation
* logic is shared via this generated artifact.
*
* Scope:
* - deriveProgressFromRoadmap(roadmapContent): count Complete rows => idempotent
* - clampPercent(completed, total): percent with 100 ceiling
*
* These two functions are the root-cause fix for issue #4.
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
`;
// ─── Pure functions defined here so they can be serialized via .toString() ───
//
// These are hand-authored pure functions (not extracted from compiled SDK output
// via .toString()) because the SDK versions are embedded inside async closures
// and cannot be cleanly extracted. The logic is a direct transcription of the
// SDK's "Root cause 1 fix" block in phase-lifecycle.ts (~line 1644).
//
// IMPORTANT: These functions must remain pure (no I/O, no closures over module
// state) so that .toString() serialization to CJS works correctly.
/**
* Derive completed_phases, total_phases, and total_plans from ROADMAP content.
*
* Root cause fix for issue #4: instead of blindly incrementing Completed Phases
* by 1 on every call, derive the count from the ROADMAP progress table.
* This makes phase.complete idempotent: running it twice produces the same value.
*
* Transcribed from sdk/src/query/phase-lifecycle.ts "Root cause 1 fix" block.
* References: ADR-3524 Section 4, issue #4.
*/
function deriveProgressFromRoadmap(roadmapContent) {
let completedPhases = null;
let totalPhases = null;
let totalPlans = null;
try {
// Count Complete rows in the progress table (Status column = "Complete").
// Pattern: row where the phase cell starts with a digit (data row, not header),
// followed by any cell content, then a "Complete" status cell.
// Handles both short form ("| 4. |") and long form ("| 01. Foundation |").
// See phase-lifecycle.ts ~line 1655 for the original SDK pattern.
const tableCompletePattern = /\|\s*\d+[^|]*\|\s*[^|]*\|\s*Complete\s*\|/gi;
const completeMatches = roadmapContent.match(tableCompletePattern);
completedPhases = completeMatches ? completeMatches.length : null;
// Count total phase rows in the progress table.
// Identify the table by looking for Phase|...|Status|...|Completed header.
const progressTableMatch = roadmapContent.match(
/\|\s*Phase\s*\|[^|]*\|[^|]*Status[^|]*\|[^|]*Completed[^|]*\|[\s\S]*?(?=\n\n|\n##|$)/i,
);
if (progressTableMatch) {
const tableText = progressTableMatch[0];
// Count data rows (rows starting with pipe then a phase number)
const dataRowPattern = /^\|\s*\d+/gm;
const dataRows = tableText.match(dataRowPattern);
totalPhases = dataRows ? dataRows.length : null;
}
// Sum plan counts from M/N columns in progress table
let totalPlansSum = 0;
const planCellPattern = /\|\s*\d+[^|]*\|\s*(\d+)\/(\d+)\s*\|/gi;
let pm;
// eslint-disable-next-line no-cond-assign
while ((pm = planCellPattern.exec(roadmapContent)) !== null) {
totalPlansSum += parseInt(pm[2], 10);
}
if (totalPlansSum > 0) totalPlans = totalPlansSum;
} catch { /* intentionally empty — fall through to existing values */ }
return { completedPhases, totalPhases, totalPlans };
}
/**
* Compute progress percent with a 100% ceiling.
*
* Root cause fix for issue #4: without this clamp, blind-increment + recalculate
* can produce >100% when Completed Phases exceeds Total Phases.
*
* References: ADR-3524 Section 4, issue #4.
*/
function clampPercent(completed, total) {
if (!total || total <= 0) return 0;
return Math.min(100, Math.round((completed / total) * 100));
}
export async function buildPhaseLifecycleCjs() {
// Serialize the pure functions via .toString() — same technique as gen-project-root.mjs.
// These functions have no external dependencies (no closures over module scope),
// so serialization is clean.
const deriveBody = deriveProgressFromRoadmap.toString();
const clampBody = clampPercent.toString();
const parts = [
BANNER.trimEnd(),
'',
'/**',
' * Derive completed_phases, total_phases, and total_plans from ROADMAP content.',
' * Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.',
' */',
deriveBody,
'',
'/**',
' * Compute progress percent clamped to 100.',
' * Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.',
' */',
clampBody,
'',
`module.exports = {`,
` deriveProgressFromRoadmap,`,
` clampPercent,`,
`};`,
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildPhaseLifecycleCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/phase-lifecycle.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);
});
}

View File

@@ -1,111 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Phase Query CJS artifact.
*
* Imports the compiled ESM output from sdk/dist/query/phase.js,
* captures pure helper functions via Function.prototype.toString(), then emits
* get-shit-done/bin/lib/phase.generated.cjs.
*
* Exported functions from sdk/src/query/phase.ts have two categories:
* (a) Pure helpers (no I/O): isCanonicalPlanFile, describeNonCanonicalPlans
* → serializable directly via Function.prototype.toString()
* (b) Async query handlers (findPhase, findPhaseByNumber, phasePlanIndex)
* → I/O stays per-side per ADR-3524 §4; NOT included in the generated artifact
*
* The phase.cjs shim continues to provide its own CJS implementations of the
* async handlers. The generated artifact provides the shared pure helpers so
* that phase.cjs can delegate to them rather than maintaining duplicates.
*
* Run: cd sdk && npm run gen:phase
* Check: node sdk/scripts/check-phase-fresh.mjs
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/phase.ts
* Regenerate: cd sdk && npm run gen:phase
*
* Phase Query Module — pure helper functions shared between the CJS CLI and SDK.
* No I/O. No async. No filesystem operations.
*
* Scope: isCanonicalPlanFile, describeNonCanonicalPlans.
* Async query handlers (findPhase, phasePlanIndex) are I/O-bound and remain
* per-side per ADR-3524 §4.
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
`;
export async function buildPhaseCjs() {
const distUrl = new URL('../dist/query/phase.js', import.meta.url);
const {
isCanonicalPlanFile,
describeNonCanonicalPlans,
} = await import(distUrl.href);
// isCanonicalPlanFile is a const arrow function; .toString() gives the arrow body
const isCanonicalPlanFileBody = `const isCanonicalPlanFile = ${isCanonicalPlanFile.toString()};`;
// The PLAN_OUTLINE_RE and PLAN_PRE_BOUNCE_RE constants are closed over by
// describeNonCanonicalPlans via the looksLikePlanFile helper. Since these are
// module-scope in the compiled ESM, we need to provide them here.
const preamble = [
`// Regex constants closed over by describeNonCanonicalPlans (from phase.js module scope)`,
`const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\\.md$/i;`,
`const PLAN_PRE_BOUNCE_RE = /-PLAN.*\\.pre-bounce\\.md$/i;`,
`const looksLikePlanFile = (f) =>`,
` /\\.md$/i.test(f)`,
` && /PLAN/i.test(f)`,
` && !PLAN_OUTLINE_RE.test(f)`,
` && !PLAN_PRE_BOUNCE_RE.test(f);`,
].join('\n');
const parts = [
BANNER.trimEnd(),
'',
isCanonicalPlanFileBody,
'',
preamble,
'',
describeNonCanonicalPlans.toString(),
'',
`module.exports = {`,
` isCanonicalPlanFile,`,
` describeNonCanonicalPlans,`,
`};`,
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildPhaseCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/phase.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);
});
}

View File

@@ -1,103 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Plan Scan CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/query/plan-scan.js,
* extracts function source via Function.prototype.toString() for exports,
* then emits get-shit-done/bin/lib/plan-scan.generated.cjs.
*
* Run: cd sdk && npm run gen:plan-scan
* Freshness check: node sdk/scripts/check-plan-scan-fresh.mjs
*/
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/query/plan-scan.js', 'sdk/src/query/plan-scan.ts');
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/plan-scan.ts
* Regenerate: cd sdk && npm run gen:plan-scan
*
* Plan Scan Module — detects plan and summary files in a phase directory.
* Supports both flat (pre-#3139) and nested (post-#3139) layouts.
*/
`;
export async function buildPlanScanCjs() {
// Load the compiled ESM module to get exports via Function.prototype.toString()
const distUrl = new URL('../dist/query/plan-scan.js', import.meta.url);
const {
isRootPlanFile,
isNestedPlanFile,
isRootSummaryFile,
isNestedSummaryFile,
scanPhasePlans,
} = await import(distUrl.href);
// Get exported function bodies via Function.prototype.toString()
const isRootPlanFileBody = isRootPlanFile.toString();
const isNestedPlanFileBody = isNestedPlanFile.toString();
const isRootSummaryFileBody = isRootSummaryFile.toString();
const isNestedSummaryFileBody = isNestedSummaryFile.toString();
const scanPhasePlansBody = scanPhasePlans.toString();
const parts = [
BANNER.trimEnd(),
'',
"const { existsSync, readdirSync } = require('node:fs');",
"const { join } = require('node:path');",
'',
'// Excluded derivative files',
'const PLAN_OUTLINE_RE = /-OUTLINE\\.md$/i;',
'const PLAN_PRE_BOUNCE_RE = /\\.pre-bounce\\.md$/i;',
'',
isRootPlanFileBody,
'',
isNestedPlanFileBody,
'',
isRootSummaryFileBody,
'',
isNestedSummaryFileBody,
'',
scanPhasePlansBody,
'',
'// CJS callers do: const scanPhasePlans = require(\'./plan-scan.cjs\')',
'// and also destructure named exports — support both call styles.',
'module.exports = scanPhasePlans;',
'module.exports.scanPhasePlans = scanPhasePlans;',
'module.exports.isRootPlanFile = isRootPlanFile;',
'module.exports.isNestedPlanFile = isNestedPlanFile;',
'module.exports.isRootSummaryFile = isRootSummaryFile;',
'module.exports.isNestedSummaryFile = isNestedSummaryFile;',
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildPlanScanCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/plan-scan.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -1,99 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Project-Root Resolution Module CJS artifact.
*
* Imports the compiled ESM output from sdk/dist/runtime/project-root.js,
* captures findProjectRoot via Function.prototype.toString(), then emits
* get-shit-done/bin/lib/project-root.generated.cjs.
*
* Run: cd sdk && npm run gen:project-root
* Freshness check: node sdk/scripts/check-project-root-fresh.mjs
*/
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/runtime/project-root.js', 'sdk/src/runtime/project-root.ts');
const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/runtime/project-root.ts
* Regenerate: cd sdk && npm run gen:project-root
*
* Project-Root Resolution Module — resolves a project root from a starting
* directory by walking the ancestor chain and applying four heuristics:
* (0) own .planning/ guard (#1362)
* (1) parent .planning/config.json sub_repos
* (2) legacy multiRepo: true + ancestor .git
* (3) .git heuristic with parent .planning/
* Bounded by FIND_PROJECT_ROOT_MAX_DEPTH ancestors. Sync I/O.
*/
`;
/**
* Build the CJS content string. Exported so the freshness-check script can
* import this function directly (Phase 3's cleaner pattern) instead of
* duplicating the logic.
*/
export async function buildProjectRootCjs() {
const distUrl = new URL('../dist/runtime/project-root.js', import.meta.url);
const { findProjectRoot, FIND_PROJECT_ROOT_MAX_DEPTH } = await import(distUrl.href);
const findProjectRootBody = findProjectRoot.toString();
// The compiled ESM uses destructured named imports:
// import { dirname, resolve, sep, relative, parse as parsePath } from 'node:path';
// import { existsSync, readFileSync, statSync } from 'node:fs';
// import { homedir } from 'node:os';
//
// In CJS we provide these as module-level constants so the function body
// can reference them as closed-over variables (same technique used in
// Phase 3 gen-workstream-inventory-builder.mjs for relative/sep/etc.).
const preamble = [
`const fs = require('fs');`,
`const path = require('path');`,
`const os = require('os');`,
`const { existsSync, readFileSync, statSync } = fs;`,
`const { dirname, resolve, sep, relative, parse: parsePath } = path;`,
`const { homedir } = os;`,
`const FIND_PROJECT_ROOT_MAX_DEPTH = ${FIND_PROJECT_ROOT_MAX_DEPTH};`,
].join('\n');
const parts = [
BANNER.trimEnd(),
'',
preamble,
'',
findProjectRootBody,
'',
`module.exports = { findProjectRoot };`,
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildProjectRootCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/project-root.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -1,149 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Schema Detect CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/query/schema-detect.js,
* extracts function source via Function.prototype.toString() for exports
* and via source-text extraction for internal constants, then emits
* get-shit-done/bin/lib/schema-detect.generated.cjs.
*
* Run: cd sdk && npm run gen:schema-detect
* Freshness check: node sdk/scripts/check-schema-detect-fresh.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/query/schema-detect.js', 'sdk/src/query/schema-detect.ts');
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/schema-detect.ts
* Regenerate: cd sdk && npm run gen:schema-detect
*
* Schema Drift Detection — detects schema-relevant file changes and verifies
* that the appropriate database push command was executed during a phase.
* This module does not read the filesystem directly.
*/
`;
/**
* Extract a top-level const declaration block (array or object literal)
* from a JS source string. Scans for `const <name> = [` or `const <name> = {`
* and captures through the balanced closing brace/bracket.
*/
function extractConstFromSource(source, name) {
// Try array form: const NAME = [
let arrayMarker = `const ${name} = [`;
let start = source.indexOf(arrayMarker);
let openChar = '[';
let closeChar = ']';
if (start === -1) {
// Try object form: const NAME = {
const objectMarker = `const ${name} = {`;
start = source.indexOf(objectMarker);
openChar = '{';
closeChar = '}';
if (start === -1) {
throw new Error(`Could not find const ${name} in compiled source`);
}
}
const braceOpen = source.indexOf(openChar, start);
if (braceOpen === -1) throw new Error(`Could not find opening ${openChar} for const ${name}`);
let depth = 0;
let i = braceOpen;
for (; i < source.length; i++) {
if (source[i] === openChar) depth++;
else if (source[i] === closeChar) {
depth--;
if (depth === 0) break;
}
}
if (depth !== 0) throw new Error(`Could not find closing ${closeChar} for const ${name}`);
// Return the full `const NAME = [...];` or `const NAME = {...};`
// Find the semicolon after the closing bracket
const afterClose = source.indexOf(';', i);
const end = afterClose !== -1 ? afterClose + 1 : i + 1;
return source.slice(start, end);
}
export async function buildSchemaDetectCjs() {
const distUrl = new URL('../dist/query/schema-detect.js', import.meta.url);
const {
detectSchemaFiles,
checkSchemaDrift,
} = await import(distUrl.href);
const compiledSource = await readFile(fileURLToPath(distUrl), 'utf-8');
// Extract non-exported constants from source text
const schemaPatternsDecl = extractConstFromSource(compiledSource, 'SCHEMA_PATTERNS');
const ormInfoDecl = extractConstFromSource(compiledSource, 'ORM_INFO');
// Get exported function bodies via Function.prototype.toString()
const detectSchemaFilesBody = detectSchemaFiles.toString();
const checkSchemaDriftBody = checkSchemaDrift.toString();
// detectSchemaOrm is not in the SDK but CJS callers may use it.
// Reconstruct it as a simple ORM_INFO lookup (same as original secrets.cjs).
const detectSchemaOrmBody = `function detectSchemaOrm(ormName) {
return ORM_INFO[ormName] || null;
}`;
const parts = [
BANNER.trimEnd(),
'',
'// ─── ORM Patterns ───────────────────────────────────────────────────────────',
schemaPatternsDecl,
'',
'// ─── Push Commands & Evidence Patterns ──────────────────────────────────────',
ormInfoDecl,
'',
'// ─── Public API ──────────────────────────────────────────────────────────────',
detectSchemaFilesBody,
'',
detectSchemaOrmBody,
'',
checkSchemaDriftBody,
'',
'module.exports = {',
' SCHEMA_PATTERNS,',
' ORM_INFO,',
' detectSchemaFiles,',
' detectSchemaOrm,',
' checkSchemaDrift,',
'};',
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildSchemaDetectCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/schema-detect.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -1,91 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Secrets CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/query/secrets.js,
* extracts function source via Function.prototype.toString() for exports,
* then emits get-shit-done/bin/lib/secrets.generated.cjs.
*
* Run: cd sdk && npm run gen:secrets
* Freshness check: node sdk/scripts/check-secrets-fresh.mjs
*/
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/query/secrets.js', 'sdk/src/query/secrets.ts');
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/secrets.ts
* Regenerate: cd sdk && npm run gen:secrets
*
* Secrets handling — masking convention for API keys and other
* credentials managed via /gsd-settings-integrations.
* This module does not read the filesystem.
*/
`;
export async function buildSecretsCjs() {
// Load the compiled ESM module to get exports via Function.prototype.toString()
const distUrl = new URL('../dist/query/secrets.js', import.meta.url);
const {
SECRET_CONFIG_KEYS,
isSecretKey,
maskSecret,
maskIfSecret,
} = await import(distUrl.href);
// Get exported function bodies via Function.prototype.toString()
const isSecretKeyBody = isSecretKey.toString();
const maskSecretBody = maskSecret.toString();
const maskIfSecretBody = maskIfSecret.toString();
// SECRET_CONFIG_KEYS is a Set — reconstruct it as a constant declaration
const secretKeys = [...SECRET_CONFIG_KEYS];
const secretKeysLiteral = secretKeys.map(k => ` '${k}',`).join('\n');
const parts = [
BANNER.trimEnd(),
'',
'const SECRET_CONFIG_KEYS = new Set([',
secretKeysLiteral,
']);',
'',
isSecretKeyBody,
'',
maskSecretBody,
'',
maskIfSecretBody,
'',
'module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret };',
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildSecretsCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/secrets.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -1,143 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the STATE.md Document Module CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/state/index.js,
* extracts function source via Function.prototype.toString() for exports
* and via source-text extraction for internal helpers, then emits
* get-shit-done/bin/lib/state-document.generated.cjs.
*
* Run: cd sdk && npx tsx scripts/gen-state-document.ts
* Freshness check: node sdk/scripts/check-state-document-fresh.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/state/index.ts
* Regenerate: cd sdk && npm run gen:state-document
*
* STATE.md Document Module — pure transforms for STATE.md text.
* This module does not read the filesystem and does not own persistence or locking.
*/
`;
/**
* Extract a top-level function declaration (non-exported) from a JS source
* string by scanning for `function <name>(` and capturing the entire body
* including balanced braces.
*/
function extractFunctionFromSource(source: string, name: string): string {
const marker = `function ${name}(`;
const start = source.indexOf(marker);
if (start === -1) {
throw new Error(`Could not find function ${name} in compiled source`);
}
// Find the opening brace
const braceOpen = source.indexOf('{', start);
if (braceOpen === -1) {
throw new Error(`Could not find opening brace for function ${name}`);
}
// Walk forward counting braces until balanced
let depth = 0;
let i = braceOpen;
for (; i < source.length; i++) {
if (source[i] === '{') depth++;
else if (source[i] === '}') {
depth--;
if (depth === 0) break;
}
}
if (depth !== 0) {
throw new Error(`Could not find closing brace for function ${name}`);
}
// Return from `function name(` through the closing `}`
return source.slice(start, i + 1);
}
export async function buildStateDocumentCjs(): Promise<string> {
// Load the compiled ESM module to get exports via Function.prototype.toString()
const distUrl = new URL('../dist/state/index.js', import.meta.url);
const {
stateExtractField,
stateReplaceField,
stateReplaceFieldWithFallback,
normalizeStateStatus,
computeProgressPercent,
shouldPreserveExistingProgress,
normalizeProgressNumbers,
} = await import(distUrl.href);
// Also read the compiled JS as text to extract non-exported helpers
const compiledSource = await readFile(fileURLToPath(distUrl), 'utf-8');
// Extract non-exported helpers from source text
const escapeRegexBody = extractFunctionFromSource(compiledSource, 'escapeRegex');
const toFiniteNumberBody = extractFunctionFromSource(compiledSource, 'toFiniteNumber');
const existingProgressExceedsDerivedBody = extractFunctionFromSource(compiledSource, 'existingProgressExceedsDerived');
// Get exported function bodies via Function.prototype.toString()
const stateExtractFieldBody = stateExtractField.toString();
const stateReplaceFieldBody = stateReplaceField.toString();
const stateReplaceFieldWithFallbackBody = stateReplaceFieldWithFallback.toString();
const normalizeStateStatusBody = normalizeStateStatus.toString();
const computeProgressPercentBody = computeProgressPercent.toString();
const shouldPreserveExistingProgressBody = shouldPreserveExistingProgress.toString();
const normalizeProgressNumbersBody = normalizeProgressNumbers.toString();
const parts: string[] = [
BANNER.trimEnd(),
'',
'// Internal helpers',
escapeRegexBody,
'',
toFiniteNumberBody,
'',
existingProgressExceedsDerivedBody,
'',
stateExtractFieldBody,
'',
stateReplaceFieldBody,
'',
stateReplaceFieldWithFallbackBody,
'',
normalizeStateStatusBody,
'',
computeProgressPercentBody,
'',
shouldPreserveExistingProgressBody,
'',
normalizeProgressNumbersBody,
'',
'module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };',
'',
];
return parts.join('\n');
}
async function main(): Promise<void> {
const content = await buildStateDocumentCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/state-document.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -1,291 +0,0 @@
#!/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';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/query/validate.js', 'sdk/src/query/validate.ts');
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);
});
}

View File

@@ -1,123 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Workstream Inventory Builder CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/workstream/builder.js,
* extracts function source via Function.prototype.toString() for exports
* and via source-text extraction for internal helpers, then emits
* get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs.
*
* Run: cd sdk && npm run gen:workstream-inventory-builder
* Freshness check: node sdk/scripts/check-workstream-inventory-builder-fresh.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/workstream/builder.js', 'sdk/src/workstream/builder.ts');
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/workstream/builder.ts
* Regenerate: cd sdk && npm run gen:workstream-inventory-builder
*
* Workstream Inventory Builder — pure projection from pre-collected
* filesystem data to typed WorkstreamInventory. No I/O. No async.
*/
`;
/**
* Extract a top-level function declaration (non-exported) from a JS source
* string by scanning for `function <name>(` and capturing the entire body
* including balanced braces.
*/
export function extractFunctionFromSource(source, name) {
const marker = `function ${name}(`;
const start = source.indexOf(marker);
if (start === -1) {
throw new Error(`Could not find function ${name} in compiled source`);
}
// Find the opening brace
const braceOpen = source.indexOf('{', start);
if (braceOpen === -1) {
throw new Error(`Could not find opening brace for function ${name}`);
}
// Walk forward counting braces until balanced
let depth = 0;
let i = braceOpen;
for (; i < source.length; i++) {
if (source[i] === '{') depth++;
else if (source[i] === '}') {
depth--;
if (depth === 0) break;
}
}
if (depth !== 0) {
throw new Error(`Could not find closing brace for function ${name}`);
}
// Return from `function name(` through the closing `}`
return source.slice(start, i + 1);
}
export async function buildWorkstreamInventoryBuilderCjs() {
// Load the compiled ESM module to get exports via Function.prototype.toString()
const distUrl = new URL('../dist/workstream/builder.js', import.meta.url);
const {
buildWorkstreamInventory,
isCompletedInventory,
} = await import(distUrl.href);
// Also read the compiled JS as text to extract non-exported helpers
const compiledSource = await readFile(fileURLToPath(distUrl), 'utf-8');
// Extract non-exported helpers from source text
const toPosixPathBody = extractFunctionFromSource(compiledSource, 'toPosixPath');
// Get exported function bodies via Function.prototype.toString()
const isCompletedInventoryBody = isCompletedInventory.toString();
const buildWorkstreamInventoryBody = buildWorkstreamInventory.toString();
const parts = [
BANNER.trimEnd(),
'',
"const path = require('path');",
'const relative = path.relative;',
'',
'// Internal helpers',
toPosixPathBody,
'',
isCompletedInventoryBody,
'',
buildWorkstreamInventoryBody,
'',
'module.exports = { buildWorkstreamInventory, isCompletedInventory };',
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildWorkstreamInventoryBuilderCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/workstream-inventory-builder.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -1,99 +0,0 @@
#!/usr/bin/env node
/**
* Generator for the Workstream Name Policy CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/workstream-name-policy.js,
* extracts function source via text transformation,
* then emits get-shit-done/bin/lib/workstream-name-policy.generated.cjs.
*
* Source-of-truth: sdk/src/workstream-name-policy.ts
*
* Run: cd sdk && npm run gen:workstream-name-policy
* Freshness check: node sdk/scripts/check-workstream-name-policy-fresh.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { requireFreshDist } from './_gen-helpers.mjs';
requireFreshDist('sdk/dist/workstream-name-policy.js', 'sdk/src/workstream-name-policy.ts');
export const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/workstream-name-policy.ts
* Regenerate: cd sdk && npm run gen:workstream-name-policy
*
* Canonical workstream name validation and slug normalization.
* Used by active-workstream-store.cjs, planning-workspace.cjs, workstream.cjs.
*/
`;
export async function buildWorkstreamNamePolicyCjs() {
// Read the compiled ESM source and transform to CJS.
const distPath = fileURLToPath(new URL('../dist/workstream-name-policy.js', import.meta.url));
const src = await readFile(distPath, 'utf-8');
// Transform ESM to CJS:
// 1. Remove import statements (none expected in this file)
// 2. Remove ESM export keywords
// 3. Remove source map comment
// 4. Remove leading jsdoc comment
// 5. Add module.exports at end
let body = src;
// Remove leading import statements (if any)
body = body.replace(/^import\s+.*?;[\r\n]*/gm, '');
// Remove ESM export keywords (keep function/const declarations)
body = body.replace(/^export (function|const) /gm, '$1 ');
// Remove source map comment
body = body.replace(/\/\/# sourceMappingURL=.*$/gm, '');
// Remove leading file-level jsdoc comment (keep module-level logic only)
body = body.replace(/^\/\*\*[\s\S]*?\*\/\n/m, '');
// Trim extra blank lines at start/end
body = body.trim();
const parts = [
BANNER.trimEnd(),
'',
body,
'',
'module.exports = {',
' validateWorkstreamName,',
' toWorkstreamSlug,',
' hasInvalidPathSegment,',
' isValidActiveWorkstreamName,',
'};',
'',
];
return parts.join('\n');
}
async function main() {
const content = await buildWorkstreamNamePolicyCjs();
const outPath = fileURLToPath(
new URL('../../get-shit-done/bin/lib/workstream-name-policy.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, not when imported.
// process.argv[1] is already an absolute filesystem path on every platform Node
// supports; comparing directly avoids the Windows URL-parsing bug where
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}