From 4c92aacca5ca14f4226e9c2a772132809f562134 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 25 May 2026 09:41:37 -0400 Subject: [PATCH] build(#188): retire sdk generator and freshness scripts --- .github/workflows/test.yml | 75 ---- package.json | 12 +- sdk/package.json | 27 -- sdk/scripts/_gen-helpers.mjs | 83 ---- sdk/scripts/check-command-aliases-fresh.mjs | 105 ------ sdk/scripts/check-configuration-fresh.mjs | 40 -- sdk/scripts/check-decisions-fresh.mjs | 31 -- sdk/scripts/check-phase-fresh.mjs | 40 -- sdk/scripts/check-phase-lifecycle-fresh.mjs | 43 --- .../check-phase-lifecycle-policy-fresh.mjs | 40 -- sdk/scripts/check-plan-scan-fresh.mjs | 31 -- sdk/scripts/check-project-root-fresh.mjs | 36 -- sdk/scripts/check-schema-detect-fresh.mjs | 31 -- sdk/scripts/check-secrets-fresh.mjs | 31 -- sdk/scripts/check-state-document-fresh.mjs | 114 ------ sdk/scripts/check-validate-fresh.mjs | 41 -- ...eck-workstream-inventory-builder-fresh.mjs | 31 -- .../check-workstream-name-policy-fresh.mjs | 31 -- sdk/scripts/gen-command-aliases.ts | 196 ---------- sdk/scripts/gen-configuration.mjs | 180 --------- sdk/scripts/gen-decisions.mjs | 103 ----- sdk/scripts/gen-phase-lifecycle-policy.mjs | 185 --------- sdk/scripts/gen-phase-lifecycle.mjs | 187 --------- sdk/scripts/gen-phase.mjs | 111 ------ sdk/scripts/gen-plan-scan.mjs | 103 ----- sdk/scripts/gen-project-root.mjs | 99 ----- sdk/scripts/gen-schema-detect.mjs | 149 -------- sdk/scripts/gen-secrets.mjs | 91 ----- sdk/scripts/gen-state-document.ts | 143 ------- sdk/scripts/gen-validate.mjs | 291 -------------- .../gen-workstream-inventory-builder.mjs | 123 ------ sdk/scripts/gen-workstream-name-policy.mjs | 99 ----- tests/configuration-generator.test.cjs | 355 ------------------ tests/decisions-generator.test.cjs | 217 ----------- .../feat-3598-generator-correctness.test.cjs | 351 ----------------- tests/gen-staleness-check.test.cjs | 201 ---------- tests/plan-scan-generator.test.cjs | 196 ---------- tests/project-root-generator.test.cjs | 152 -------- tests/schema-detect-generator.test.cjs | 195 ---------- tests/secrets-generator.test.cjs | 136 ------- tests/state-document-generator.test.cjs | 261 ------------- ...tream-inventory-builder-generator.test.cjs | 159 -------- .../workstream-name-policy-generator.test.cjs | 145 ------- 43 files changed, 1 insertion(+), 5269 deletions(-) delete mode 100644 sdk/scripts/_gen-helpers.mjs delete mode 100644 sdk/scripts/check-command-aliases-fresh.mjs delete mode 100644 sdk/scripts/check-configuration-fresh.mjs delete mode 100644 sdk/scripts/check-decisions-fresh.mjs delete mode 100644 sdk/scripts/check-phase-fresh.mjs delete mode 100644 sdk/scripts/check-phase-lifecycle-fresh.mjs delete mode 100644 sdk/scripts/check-phase-lifecycle-policy-fresh.mjs delete mode 100644 sdk/scripts/check-plan-scan-fresh.mjs delete mode 100644 sdk/scripts/check-project-root-fresh.mjs delete mode 100644 sdk/scripts/check-schema-detect-fresh.mjs delete mode 100644 sdk/scripts/check-secrets-fresh.mjs delete mode 100644 sdk/scripts/check-state-document-fresh.mjs delete mode 100644 sdk/scripts/check-validate-fresh.mjs delete mode 100644 sdk/scripts/check-workstream-inventory-builder-fresh.mjs delete mode 100644 sdk/scripts/check-workstream-name-policy-fresh.mjs delete mode 100644 sdk/scripts/gen-command-aliases.ts delete mode 100644 sdk/scripts/gen-configuration.mjs delete mode 100644 sdk/scripts/gen-decisions.mjs delete mode 100644 sdk/scripts/gen-phase-lifecycle-policy.mjs delete mode 100644 sdk/scripts/gen-phase-lifecycle.mjs delete mode 100644 sdk/scripts/gen-phase.mjs delete mode 100644 sdk/scripts/gen-plan-scan.mjs delete mode 100644 sdk/scripts/gen-project-root.mjs delete mode 100644 sdk/scripts/gen-schema-detect.mjs delete mode 100644 sdk/scripts/gen-secrets.mjs delete mode 100644 sdk/scripts/gen-state-document.ts delete mode 100644 sdk/scripts/gen-validate.mjs delete mode 100644 sdk/scripts/gen-workstream-inventory-builder.mjs delete mode 100644 sdk/scripts/gen-workstream-name-policy.mjs delete mode 100644 tests/configuration-generator.test.cjs delete mode 100644 tests/decisions-generator.test.cjs delete mode 100644 tests/feat-3598-generator-correctness.test.cjs delete mode 100644 tests/gen-staleness-check.test.cjs delete mode 100644 tests/plan-scan-generator.test.cjs delete mode 100644 tests/project-root-generator.test.cjs delete mode 100644 tests/schema-detect-generator.test.cjs delete mode 100644 tests/secrets-generator.test.cjs delete mode 100644 tests/state-document-generator.test.cjs delete mode 100644 tests/workstream-inventory-builder-generator.test.cjs delete mode 100644 tests/workstream-name-policy-generator.test.cjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac8b8380a..0faf0ca62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -177,81 +177,6 @@ jobs: shell: bash run: cd sdk && npx vitest run src/query/command-seam-coverage.test.ts - - name: SDK generated alias artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-command-aliases-fresh.mjs - - - name: SDK generated state-document artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-state-document-fresh.mjs - - - name: SDK generated configuration artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-configuration-fresh.mjs - - - name: SDK generated workstream-inventory-builder artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-workstream-inventory-builder-fresh.mjs - - - name: SDK generated project-root artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-project-root-fresh.mjs - - - name: SDK generated plan-scan artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-plan-scan-fresh.mjs - - - name: SDK generated secrets artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-secrets-fresh.mjs - - - name: SDK generated schema-detect artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-schema-detect-fresh.mjs - - - name: SDK generated decisions artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-decisions-fresh.mjs - - - name: SDK generated workstream-name-policy artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-workstream-name-policy-fresh.mjs - - - name: SDK generated validate artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-validate-fresh.mjs - - - name: SDK generated phase artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-phase-fresh.mjs - - - name: SDK generated phase-lifecycle artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-phase-lifecycle-fresh.mjs - - - name: SDK generated phase-lifecycle-policy artifact drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node sdk/scripts/check-phase-lifecycle-policy-fresh.mjs - - - name: Shared Module hand-sync drift check - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 - shell: bash - run: node scripts/lint-shared-module-handsync.cjs - # Split lanes (issue #3597). Unit is the fast default lane; integration # and security run alongside it on every PR. `install` and `slow` are # skipped on PR CI by design — they run on the weekly windows-compat diff --git a/package.json b/package.json index e6d0d7c2b..5b15b0ed0 100644 --- a/package.json +++ b/package.json @@ -64,19 +64,9 @@ "scripts": { "check:env": "bash scripts/check-env.sh", "check:integrity": "./scripts/check-npm-integrity.sh", + "build": "tsc -b", "build:hooks": "node scripts/build-hooks.js", "build:sdk": "cd sdk && npm ci && npm run build", - "check:alias-drift": "cd sdk && npm run check:alias-drift", - "check:state-document-fresh": "cd sdk && npm run check:state-document-fresh", - "check:configuration-fresh": "cd sdk && npm run check:configuration-fresh", - "check:workstream-inventory-builder-fresh": "cd sdk && npm run check:workstream-inventory-builder-fresh", - "check:project-root-fresh": "cd sdk && npm run check:project-root-fresh", - "check:plan-scan-fresh": "cd sdk && npm run check:plan-scan-fresh", - "check:secrets-fresh": "cd sdk && npm run check:secrets-fresh", - "check:schema-detect-fresh": "cd sdk && npm run check:schema-detect-fresh", - "check:decisions-fresh": "cd sdk && npm run check:decisions-fresh", - "check:workstream-name-policy-fresh": "cd sdk && npm run check:workstream-name-policy-fresh", - "check:validate-fresh": "cd sdk && npm run check:validate-fresh", "prepublishOnly": "npm run build:hooks && npm run build:sdk", "pretest": "npm run build:sdk && npm run lint:skill-deps", "pretest:coverage": "npm run build:sdk", diff --git a/sdk/package.json b/sdk/package.json index 24f2d495e..6fc62fa19 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -38,33 +38,6 @@ }, "scripts": { "build": "tsc", - "check:alias-drift": "npm run build && node scripts/check-command-aliases-fresh.mjs", - "gen:state-document": "npm run build && npx tsx scripts/gen-state-document.ts", - "check:state-document-fresh": "npm run build && node scripts/check-state-document-fresh.mjs", - "gen:configuration": "npm run build && node scripts/gen-configuration.mjs", - "check:configuration-fresh": "npm run build && node scripts/check-configuration-fresh.mjs", - "gen:workstream-inventory-builder": "npm run build && node scripts/gen-workstream-inventory-builder.mjs", - "check:workstream-inventory-builder-fresh": "npm run build && node scripts/check-workstream-inventory-builder-fresh.mjs", - "gen:project-root": "npm run build && node scripts/gen-project-root.mjs", - "check:project-root-fresh": "npm run build && node scripts/check-project-root-fresh.mjs", - "gen:plan-scan": "npm run build && node scripts/gen-plan-scan.mjs", - "check:plan-scan-fresh": "npm run build && node scripts/check-plan-scan-fresh.mjs", - "gen:secrets": "npm run build && node scripts/gen-secrets.mjs", - "check:secrets-fresh": "npm run build && node scripts/check-secrets-fresh.mjs", - "gen:schema-detect": "npm run build && node scripts/gen-schema-detect.mjs", - "check:schema-detect-fresh": "npm run build && node scripts/check-schema-detect-fresh.mjs", - "gen:decisions": "npm run build && node scripts/gen-decisions.mjs", - "check:decisions-fresh": "npm run build && node scripts/check-decisions-fresh.mjs", - "gen:workstream-name-policy": "npm run build && node scripts/gen-workstream-name-policy.mjs", - "check:workstream-name-policy-fresh": "npm run build && node scripts/check-workstream-name-policy-fresh.mjs", - "gen:validate": "npm run build && node scripts/gen-validate.mjs", - "check:validate-fresh": "npm run build && node scripts/check-validate-fresh.mjs", - "gen:phase": "npm run build && node scripts/gen-phase.mjs", - "check:phase-fresh": "npm run build && node scripts/check-phase-fresh.mjs", - "gen:phase-lifecycle": "node scripts/gen-phase-lifecycle.mjs", - "check:phase-lifecycle-fresh": "node scripts/check-phase-lifecycle-fresh.mjs", - "gen:phase-lifecycle-policy": "npm run build && node scripts/gen-phase-lifecycle-policy.mjs", - "check:phase-lifecycle-policy-fresh": "npm run build && node scripts/check-phase-lifecycle-policy-fresh.mjs", "prepublishOnly": "rm -rf dist && tsc && chmod +x dist/cli.js", "test": "vitest run", "test:unit": "vitest run --project unit", diff --git a/sdk/scripts/_gen-helpers.mjs b/sdk/scripts/_gen-helpers.mjs deleted file mode 100644 index 09b75fc2e..000000000 --- a/sdk/scripts/_gen-helpers.mjs +++ /dev/null @@ -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); - } -} diff --git a/sdk/scripts/check-command-aliases-fresh.mjs b/sdk/scripts/check-command-aliases-fresh.mjs deleted file mode 100644 index e93f9bc67..000000000 --- a/sdk/scripts/check-command-aliases-fresh.mjs +++ /dev/null @@ -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'); diff --git a/sdk/scripts/check-configuration-fresh.mjs b/sdk/scripts/check-configuration-fresh.mjs deleted file mode 100644 index a4023e3df..000000000 --- a/sdk/scripts/check-configuration-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-decisions-fresh.mjs b/sdk/scripts/check-decisions-fresh.mjs deleted file mode 100644 index 338dc37ae..000000000 --- a/sdk/scripts/check-decisions-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-phase-fresh.mjs b/sdk/scripts/check-phase-fresh.mjs deleted file mode 100644 index 5ba614ce0..000000000 --- a/sdk/scripts/check-phase-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-phase-lifecycle-fresh.mjs b/sdk/scripts/check-phase-lifecycle-fresh.mjs deleted file mode 100644 index d55bbacbe..000000000 --- a/sdk/scripts/check-phase-lifecycle-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-phase-lifecycle-policy-fresh.mjs b/sdk/scripts/check-phase-lifecycle-policy-fresh.mjs deleted file mode 100644 index 4f63b3b27..000000000 --- a/sdk/scripts/check-phase-lifecycle-policy-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-plan-scan-fresh.mjs b/sdk/scripts/check-plan-scan-fresh.mjs deleted file mode 100644 index 4f01d2e15..000000000 --- a/sdk/scripts/check-plan-scan-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-project-root-fresh.mjs b/sdk/scripts/check-project-root-fresh.mjs deleted file mode 100644 index 75cc7fc23..000000000 --- a/sdk/scripts/check-project-root-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-schema-detect-fresh.mjs b/sdk/scripts/check-schema-detect-fresh.mjs deleted file mode 100644 index 7d53d3a03..000000000 --- a/sdk/scripts/check-schema-detect-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-secrets-fresh.mjs b/sdk/scripts/check-secrets-fresh.mjs deleted file mode 100644 index 1e82977ea..000000000 --- a/sdk/scripts/check-secrets-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-state-document-fresh.mjs b/sdk/scripts/check-state-document-fresh.mjs deleted file mode 100644 index 369a07480..000000000 --- a/sdk/scripts/check-state-document-fresh.mjs +++ /dev/null @@ -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 (` 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); -} diff --git a/sdk/scripts/check-validate-fresh.mjs b/sdk/scripts/check-validate-fresh.mjs deleted file mode 100644 index bf4c009a3..000000000 --- a/sdk/scripts/check-validate-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-workstream-inventory-builder-fresh.mjs b/sdk/scripts/check-workstream-inventory-builder-fresh.mjs deleted file mode 100644 index e48403c10..000000000 --- a/sdk/scripts/check-workstream-inventory-builder-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/check-workstream-name-policy-fresh.mjs b/sdk/scripts/check-workstream-name-policy-fresh.mjs deleted file mode 100644 index 2db5d6475..000000000 --- a/sdk/scripts/check-workstream-name-policy-fresh.mjs +++ /dev/null @@ -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); -} diff --git a/sdk/scripts/gen-command-aliases.ts b/sdk/scripts/gen-command-aliases.ts deleted file mode 100644 index cbf2bfd12..000000000 --- a/sdk/scripts/gen-command-aliases.ts +++ /dev/null @@ -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 { - 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(STATE_COMMAND_ALIASES.map((entry) => entry.subcommand));', - 'export const VERIFY_SUBCOMMANDS = new Set(VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand));', - 'export const INIT_SUBCOMMANDS = new Set(INIT_COMMAND_ALIASES.map((entry) => entry.subcommand));', - 'export const PHASE_SUBCOMMANDS = new Set(PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand));', - 'export const PHASES_SUBCOMMANDS = new Set(PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand));', - 'export const VALIDATE_SUBCOMMANDS = new Set(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand));', - 'export const ROADMAP_SUBCOMMANDS = new Set(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); -}); diff --git a/sdk/scripts/gen-configuration.mjs b/sdk/scripts/gen-configuration.mjs deleted file mode 100644 index 35d4be06a..000000000 --- a/sdk/scripts/gen-configuration.mjs +++ /dev/null @@ -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 (` (or `async function (`) 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}`); -} diff --git a/sdk/scripts/gen-decisions.mjs b/sdk/scripts/gen-decisions.mjs deleted file mode 100644 index f58019df9..000000000 --- a/sdk/scripts/gen-decisions.mjs +++ /dev/null @@ -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 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); - }); -} diff --git a/sdk/scripts/gen-phase-lifecycle-policy.mjs b/sdk/scripts/gen-phase-lifecycle-policy.mjs deleted file mode 100644 index 48fe179fc..000000000 --- a/sdk/scripts/gen-phase-lifecycle-policy.mjs +++ /dev/null @@ -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); - }); -} diff --git a/sdk/scripts/gen-phase-lifecycle.mjs b/sdk/scripts/gen-phase-lifecycle.mjs deleted file mode 100644 index a7a837ce6..000000000 --- a/sdk/scripts/gen-phase-lifecycle.mjs +++ /dev/null @@ -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); - }); -} diff --git a/sdk/scripts/gen-phase.mjs b/sdk/scripts/gen-phase.mjs deleted file mode 100644 index 5d1feb6db..000000000 --- a/sdk/scripts/gen-phase.mjs +++ /dev/null @@ -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); - }); -} diff --git a/sdk/scripts/gen-plan-scan.mjs b/sdk/scripts/gen-plan-scan.mjs deleted file mode 100644 index 1e1fc8251..000000000 --- a/sdk/scripts/gen-plan-scan.mjs +++ /dev/null @@ -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); - }); -} diff --git a/sdk/scripts/gen-project-root.mjs b/sdk/scripts/gen-project-root.mjs deleted file mode 100644 index 7c781c729..000000000 --- a/sdk/scripts/gen-project-root.mjs +++ /dev/null @@ -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); - }); -} diff --git a/sdk/scripts/gen-schema-detect.mjs b/sdk/scripts/gen-schema-detect.mjs deleted file mode 100644 index 0602ddbf0..000000000 --- a/sdk/scripts/gen-schema-detect.mjs +++ /dev/null @@ -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 = [` or `const = {` - * 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); - }); -} diff --git a/sdk/scripts/gen-secrets.mjs b/sdk/scripts/gen-secrets.mjs deleted file mode 100644 index d9d5ef7e5..000000000 --- a/sdk/scripts/gen-secrets.mjs +++ /dev/null @@ -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); - }); -} diff --git a/sdk/scripts/gen-state-document.ts b/sdk/scripts/gen-state-document.ts deleted file mode 100644 index 4e17a53ed..000000000 --- a/sdk/scripts/gen-state-document.ts +++ /dev/null @@ -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 (` 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 { - // 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 { - 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); - }); -} diff --git a/sdk/scripts/gen-validate.mjs b/sdk/scripts/gen-validate.mjs deleted file mode 100644 index a71bc37bc..000000000 --- a/sdk/scripts/gen-validate.mjs +++ /dev/null @@ -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 = //;` 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 = ...` and `export const = ...` - 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 = ` (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 = `) - 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 () {` 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); - }); -} diff --git a/sdk/scripts/gen-workstream-inventory-builder.mjs b/sdk/scripts/gen-workstream-inventory-builder.mjs deleted file mode 100644 index 1b1a74da4..000000000 --- a/sdk/scripts/gen-workstream-inventory-builder.mjs +++ /dev/null @@ -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 (` 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); - }); -} diff --git a/sdk/scripts/gen-workstream-name-policy.mjs b/sdk/scripts/gen-workstream-name-policy.mjs deleted file mode 100644 index 0bf334dea..000000000 --- a/sdk/scripts/gen-workstream-name-policy.mjs +++ /dev/null @@ -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); - }); -} diff --git a/tests/configuration-generator.test.cjs b/tests/configuration-generator.test.cjs deleted file mode 100644 index f5995652a..000000000 --- a/tests/configuration-generator.test.cjs +++ /dev/null @@ -1,355 +0,0 @@ -'use strict'; - -/** - * Parity test: configuration.generated.cjs (CJS) vs sdk/dist/config/index.js (ESM). - * - * For every fixture in the vitest pinning tests, asserts that both sides produce - * identical output. This ensures the generator faithfully replicates the TS source. - * - * Uses node:test + dynamic import() for the ESM side. - */ - -const { describe, test, before } = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const os = require('node:os'); - -// ─── CJS side (synchronous require) ────────────────────────────────────────── - -const cjs = require('../get-shit-done/bin/lib/configuration.generated.cjs'); - -// ─── Helpers ────────────────────────────────────────────────────────────────── - -function makeTmpProject() { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-')); - fs.mkdirSync(path.join(dir, '.planning'), { recursive: true }); - return dir; -} - -function writeConfig(dir, data) { - fs.writeFileSync(path.join(dir, '.planning', 'config.json'), JSON.stringify(data, null, 2)); -} - -function readConfigRaw(dir) { - return fs.readFileSync(path.join(dir, '.planning', 'config.json'), 'utf-8'); -} - -function cleanup(dir) { - fs.rmSync(dir, { recursive: true, force: true }); -} - -// ─── ESM side (loaded once via before()) ───────────────────────────────────── - -let esm; - -before(async () => { - esm = await import('../sdk/dist/config/index.js'); -}); - -// ─── Parity helper ──────────────────────────────────────────────────────────── - -/** - * Deep-equal assertion that normalizes Sets to arrays for comparison. - */ -function assertDeepEqual(label, actual, expected) { - const normalize = (v) => JSON.parse(JSON.stringify(v, (_k, val) => - val instanceof Set ? [...val].sort() : val - )); - assert.deepStrictEqual(normalize(actual), normalize(expected), `${label} mismatch`); -} - -// ─── CONFIG_DEFAULTS parity ─────────────────────────────────────────────────── - -describe('CONFIG_DEFAULTS parity', () => { - test('model_profile matches', () => { - assert.strictEqual(cjs.CONFIG_DEFAULTS.model_profile, esm.CONFIG_DEFAULTS.model_profile); - }); - - test('git section matches', () => { - assertDeepEqual('git', cjs.CONFIG_DEFAULTS.git, esm.CONFIG_DEFAULTS.git); - }); - - test('workflow section matches', () => { - assertDeepEqual('workflow', cjs.CONFIG_DEFAULTS.workflow, esm.CONFIG_DEFAULTS.workflow); - }); - - test('hooks section matches', () => { - assertDeepEqual('hooks', cjs.CONFIG_DEFAULTS.hooks, esm.CONFIG_DEFAULTS.hooks); - }); -}); - -// ─── VALID_CONFIG_KEYS parity ───────────────────────────────────────────────── - -describe('VALID_CONFIG_KEYS parity', () => { - test('same size', () => { - assert.strictEqual(cjs.VALID_CONFIG_KEYS.size, esm.VALID_CONFIG_KEYS.size); - }); - - test('same entries', () => { - for (const key of esm.VALID_CONFIG_KEYS) { - assert.ok(cjs.VALID_CONFIG_KEYS.has(key), `CJS missing key: ${key}`); - } - for (const key of cjs.VALID_CONFIG_KEYS) { - assert.ok(esm.VALID_CONFIG_KEYS.has(key), `ESM missing key: ${key}`); - } - }); -}); - -// ─── DYNAMIC_KEY_PATTERNS parity ───────────────────────────────────────────── - -describe('DYNAMIC_KEY_PATTERNS parity', () => { - test('same length', () => { - assert.strictEqual(cjs.DYNAMIC_KEY_PATTERNS.length, esm.DYNAMIC_KEY_PATTERNS.length); - }); - - test('same topLevel and source strings', () => { - for (let i = 0; i < esm.DYNAMIC_KEY_PATTERNS.length; i++) { - assert.strictEqual(cjs.DYNAMIC_KEY_PATTERNS[i].topLevel, esm.DYNAMIC_KEY_PATTERNS[i].topLevel, `topLevel[${i}]`); - assert.strictEqual(cjs.DYNAMIC_KEY_PATTERNS[i].source, esm.DYNAMIC_KEY_PATTERNS[i].source, `source[${i}]`); - } - }); - - test('test functions produce same results', () => { - const sampleKeys = [ - 'agent_skills.planner', - 'agent_skills.executor', - 'review.models.ollama', - 'features.thinking_partner', - 'claude_md_assembly.blocks.intro', - 'model_profile_overrides.openai.opus', - 'models.planning', - 'dynamic_routing.enabled', - 'model_overrides.my-agent', - 'workflow.research', - 'unknown_key', - ]; - for (const key of sampleKeys) { - for (let i = 0; i < esm.DYNAMIC_KEY_PATTERNS.length; i++) { - const esmResult = esm.DYNAMIC_KEY_PATTERNS[i].test(key); - const cjsResult = cjs.DYNAMIC_KEY_PATTERNS[i].test(key); - assert.strictEqual(cjsResult, esmResult, `pattern[${i}].test('${key}')`); - } - } - }); -}); - -// ─── normalizeLegacyKeys parity ─────────────────────────────────────────────── - -describe('normalizeLegacyKeys parity', () => { - test('branching_strategy migration', () => { - const input = { branching_strategy: 'phase' }; - const esmR = esm.normalizeLegacyKeys(input); - const cjsR = cjs.normalizeLegacyKeys(input); - assertDeepEqual('parsed', cjsR.parsed, esmR.parsed); - assertDeepEqual('normalizations', cjsR.normalizations, esmR.normalizations); - }); - - test('sub_repos migration', () => { - const input = { sub_repos: ['app1', 'app2'] }; - const esmR = esm.normalizeLegacyKeys(input); - const cjsR = cjs.normalizeLegacyKeys(input); - assertDeepEqual('parsed', cjsR.parsed, esmR.parsed); - assertDeepEqual('normalizations', cjsR.normalizations, esmR.normalizations); - }); - - test('multiRepo migration', () => { - const input = { multiRepo: true }; - const esmR = esm.normalizeLegacyKeys(input); - const cjsR = cjs.normalizeLegacyKeys(input); - assertDeepEqual('parsed', cjsR.parsed, esmR.parsed); - assert.strictEqual(cjsR.normalizations.length, esmR.normalizations.length); - assert.strictEqual(cjsR.normalizations[0].requiresFilesystem, esmR.normalizations[0].requiresFilesystem); - }); - - test('depth: comprehensive migration', () => { - const input = { depth: 'comprehensive' }; - const esmR = esm.normalizeLegacyKeys(input); - const cjsR = cjs.normalizeLegacyKeys(input); - assertDeepEqual('parsed', cjsR.parsed, esmR.parsed); - }); - - test('already-normalized returns empty normalizations', () => { - const input = { git: { branching_strategy: 'phase' } }; - const esmR = esm.normalizeLegacyKeys(input); - const cjsR = cjs.normalizeLegacyKeys(input); - assert.strictEqual(cjsR.normalizations.length, 0); - assert.strictEqual(esmR.normalizations.length, 0); - }); - - test('idempotent — second call returns empty normalizations', () => { - const input = { branching_strategy: 'milestone' }; - const first_cjs = cjs.normalizeLegacyKeys(input); - const second_cjs = cjs.normalizeLegacyKeys(first_cjs.parsed); - const first_esm = esm.normalizeLegacyKeys(input); - const second_esm = esm.normalizeLegacyKeys(first_esm.parsed); - assert.strictEqual(second_cjs.normalizations.length, 0); - assert.strictEqual(second_esm.normalizations.length, 0); - assertDeepEqual('second_parsed', second_cjs.parsed, second_esm.parsed); - }); -}); - -// ─── mergeDefaults parity ───────────────────────────────────────────────────── - -describe('mergeDefaults parity', () => { - test('empty input returns CONFIG_DEFAULTS shape', () => { - const esmR = esm.mergeDefaults({}); - const cjsR = cjs.mergeDefaults({}); - assert.strictEqual(cjsR.model_profile, esmR.model_profile); - assertDeepEqual('git', cjsR.git, esmR.git); - assertDeepEqual('workflow', cjsR.workflow, esmR.workflow); - assertDeepEqual('hooks', cjsR.hooks, esmR.hooks); - }); - - test('partial nested preserves siblings', () => { - const input = { git: { base_branch: 'main' } }; - const esmR = esm.mergeDefaults(input); - const cjsR = cjs.mergeDefaults(input); - assert.strictEqual(cjsR.git.base_branch, esmR.git.base_branch); - assert.strictEqual(cjsR.git.branching_strategy, esmR.git.branching_strategy); - }); - - test('boolean false preserved', () => { - const input = { workflow: { research: false } }; - const esmR = esm.mergeDefaults(input); - const cjsR = cjs.mergeDefaults(input); - assert.strictEqual(cjsR.workflow.research, false); - assert.strictEqual(esmR.workflow.research, false); - }); - - test('null preserved', () => { - const input = { project_code: null }; - const esmR = esm.mergeDefaults(input); - const cjsR = cjs.mergeDefaults(input); - assert.strictEqual(cjsR.project_code, null); - assert.strictEqual(esmR.project_code, null); - }); -}); - -// ─── loadConfig parity ──────────────────────────────────────────────────────── - -describe('loadConfig parity', () => { - test('missing config.json returns defaults', async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-lc-')); - try { - const esmR = await esm.loadConfig(dir); - const cjsR = await cjs.loadConfig(dir); - assert.strictEqual(cjsR.model_profile, esmR.model_profile); - assertDeepEqual('git', cjsR.git, esmR.git); - } finally { - cleanup(dir); - } - }); - - test('empty {} config.json returns defaults', async () => { - const dir = makeTmpProject(); - writeConfig(dir, {}); - try { - const esmR = await esm.loadConfig(dir); - const cjsR = await cjs.loadConfig(dir); - assert.strictEqual(cjsR.model_profile, esmR.model_profile); - } finally { - cleanup(dir); - } - }); - - test('nested git.branching_strategy preserved', async () => { - const dir = makeTmpProject(); - writeConfig(dir, { git: { branching_strategy: 'phase' } }); - try { - const esmR = await esm.loadConfig(dir); - const cjsR = await cjs.loadConfig(dir); - assert.strictEqual(cjsR.git.branching_strategy, 'phase'); - assert.strictEqual(esmR.git.branching_strategy, 'phase'); - } finally { - cleanup(dir); - } - }); - - test('legacy top-level branching_strategy normalized, disk unchanged', async () => { - const dir = makeTmpProject(); - writeConfig(dir, { branching_strategy: 'phase' }); - const before = readConfigRaw(dir); - try { - const esmR = await esm.loadConfig(dir); - const cjsR = await cjs.loadConfig(dir); - assert.strictEqual(cjsR.git.branching_strategy, 'phase'); - assert.strictEqual(esmR.git.branching_strategy, 'phase'); - // Disk must be unchanged - assert.strictEqual(readConfigRaw(dir), before); - } finally { - cleanup(dir); - } - }); - - test('throws on malformed JSON', async () => { - const dir = makeTmpProject(); - fs.writeFileSync(path.join(dir, '.planning', 'config.json'), '{bad json'); - try { - await assert.rejects(() => cjs.loadConfig(dir), /parse|invalid|json/i); - await assert.rejects(() => esm.loadConfig(dir), /parse|invalid|json/i); - } finally { - cleanup(dir); - } - }); -}); - -// ─── migrateOnDisk parity ───────────────────────────────────────────────────── - -describe('migrateOnDisk parity', () => { - test('no-op for already-normalized config', async () => { - const dir = makeTmpProject(); - writeConfig(dir, { git: { branching_strategy: 'phase' } }); - try { - const esmR = await esm.migrateOnDisk(dir); - // Reset file for CJS test - writeConfig(dir, { git: { branching_strategy: 'phase' } }); - const cjsR = await cjs.migrateOnDisk(dir); - assert.strictEqual(cjsR.migrated, false); - assert.strictEqual(esmR.migrated, false); - assert.strictEqual(cjsR.wrote, null); - assert.strictEqual(esmR.wrote, null); - } finally { - cleanup(dir); - } - }); - - test('migrates legacy key and writes disk', async () => { - const dirEsm = makeTmpProject(); - const dirCjs = makeTmpProject(); - writeConfig(dirEsm, { branching_strategy: 'phase' }); - writeConfig(dirCjs, { branching_strategy: 'phase' }); - try { - const esmR = await esm.migrateOnDisk(dirEsm); - const cjsR = await cjs.migrateOnDisk(dirCjs); - assert.strictEqual(cjsR.migrated, true); - assert.strictEqual(esmR.migrated, true); - assert.ok(cjsR.wrote !== null); - assert.ok(esmR.wrote !== null); - // Both should have normalized the disk file - const cjsDisk = JSON.parse(readConfigRaw(dirCjs)); - const esmDisk = JSON.parse(readConfigRaw(dirEsm)); - assert.strictEqual(cjsDisk.branching_strategy, undefined); - assert.strictEqual(esmDisk.branching_strategy, undefined); - assert.strictEqual(cjsDisk.git?.branching_strategy, 'phase'); - assert.strictEqual(esmDisk.git?.branching_strategy, 'phase'); - } finally { - cleanup(dirEsm); - cleanup(dirCjs); - } - }); - - test('missing file returns migrated:false, wrote:null', async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-md-')); - try { - const esmR = await esm.migrateOnDisk(dir); - const cjsR = await cjs.migrateOnDisk(dir); - assert.strictEqual(cjsR.migrated, false); - assert.strictEqual(esmR.migrated, false); - assert.strictEqual(cjsR.wrote, null); - assert.strictEqual(esmR.wrote, null); - } finally { - cleanup(dir); - } - }); -}); diff --git a/tests/decisions-generator.test.cjs b/tests/decisions-generator.test.cjs deleted file mode 100644 index 267dd1270..000000000 --- a/tests/decisions-generator.test.cjs +++ /dev/null @@ -1,217 +0,0 @@ -'use strict'; - -/** - * Parity test: decisions.generated.cjs vs sdk/src/query/decisions.ts - * - * Verifies that the generated CJS artifact matches the SDK source-of-truth - * for all supported ID formats (numeric and alphanumeric) and edge cases. - * - * Covers: Phase 6 (#3575) MIGRATE_ME resolution for decisions.cjs. - */ - -const assert = require('assert'); -const { describe, test } = require('node:test'); -const { parseDecisions } = require('../get-shit-done/bin/lib/decisions.cjs'); - -// ─── Core parity: numeric IDs (legacy format) ──────────────────────────────── - -describe('decisions-generator parity — numeric IDs (legacy)', () => { - test('extracts D-NN entries with {id, text}', () => { - const md = ` - -## Implementation Decisions - -### Auth -- **D-01:** Use OAuth 2.0 with PKCE -- **D-02:** Session storage in Redis - -### Storage -- **D-03:** Postgres 15 with pgvector - -`; - const ds = parseDecisions(md); - assert.deepStrictEqual(ds.map(d => d.id), ['D-01', 'D-02', 'D-03']); - assert.strictEqual(ds[0].text, 'Use OAuth 2.0 with PKCE'); - }); - - test('returns [] when no block is present', () => { - assert.deepStrictEqual(parseDecisions('# Just a header\nno decisions here'), []); - }); - - test('returns [] for empty / null / undefined input', () => { - assert.deepStrictEqual(parseDecisions(''), []); - assert.deepStrictEqual(parseDecisions(null), []); - assert.deepStrictEqual(parseDecisions(undefined), []); - }); - - test('ignores D-IDs outside the block', () => { - const md = ` -Top of file. - **D-99:** Not a real decision (outside block). - -- **D-01:** Real decision - -After the block. - **D-77:** Also not real. -`; - const ds = parseDecisions(md); - assert.deepStrictEqual(ds.map(d => d.id), ['D-01']); - }); -}); - -// ─── Phase 6 extension: alphanumeric IDs ───────────────────────────────────── - -describe('decisions-generator parity — alphanumeric IDs (Phase 6 extension)', () => { - test('accepts alphanumeric IDs: D-INFRA-01', () => { - const md = ` - -### Infrastructure -- **D-INFRA-01:** Use Kubernetes for orchestration - -`; - const ds = parseDecisions(md); - assert.strictEqual(ds.length, 1); - assert.strictEqual(ds[0].id, 'D-INFRA-01'); - assert.strictEqual(ds[0].text, 'Use Kubernetes for orchestration'); - }); - - test('accepts alphanumeric IDs: D-42 (single numeric)', () => { - const md = ` - -### Architecture -- **D-42:** Use microservices - -`; - const ds = parseDecisions(md); - assert.strictEqual(ds[0].id, 'D-42'); - }); - - test('accepts mixed numeric and alphanumeric IDs in same block', () => { - const md = ` - -### Planning -- **D-01:** First numeric decision -- **D-FOO_BAR:** Alphanumeric with underscore -- **D-ARCH-123:** Mixed alphanumeric with hyphen - -`; - const ds = parseDecisions(md); - const ids = ds.map(d => d.id); - assert.ok(ids.includes('D-01'), 'should have D-01'); - assert.ok(ids.includes('D-FOO_BAR'), 'should have D-FOO_BAR'); - assert.ok(ids.includes('D-ARCH-123'), 'should have D-ARCH-123'); - }); - - test('CJS callers can use {id, text} shape — extra fields present but safe to ignore', () => { - const md = ` - -### Category -- **D-INFRA-01:** Database selection - -`; - const ds = parseDecisions(md); - const d = ds[0]; - // Verify {id, text} is present as CJS callers expect - assert.strictEqual(typeof d.id, 'string'); - assert.strictEqual(typeof d.text, 'string'); - // Extra SDK fields are present but can be ignored - assert.ok('category' in d, 'category field present'); - assert.ok('tags' in d, 'tags field present'); - assert.ok('trackable' in d, 'trackable field present'); - }); -}); - -// ─── Richer schema fields (SDK extension) ──────────────────────────────────── - -describe('decisions-generator parity — richer schema', () => { - test('marks decisions under "Claude\'s Discretion" as non-trackable', () => { - const md = ` - -### Claude's Discretion -- **D-50:** Internal naming is flexible - -`; - const ds = parseDecisions(md); - assert.strictEqual(ds[0].trackable, false); - }); - - test('marks [informational] tagged decisions as non-trackable', () => { - const md = ` - -### Info -- **D-03 [informational]:** Background context only - -`; - const ds = parseDecisions(md); - assert.strictEqual(ds[0].trackable, false); - assert.ok(ds[0].tags.includes('informational')); - }); - - test('marks [folded] tagged decisions as non-trackable', () => { - const md = ` - -### Deferred -- **D-05 [folded]:** Will handle later - -`; - const ds = parseDecisions(md); - assert.strictEqual(ds[0].trackable, false); - }); - - test('extracts category from ### heading', () => { - const md = ` - -### Storage Backend -- **D-01:** Use PostgreSQL - -`; - const ds = parseDecisions(md); - assert.strictEqual(ds[0].category, 'Storage Backend'); - }); - - test('parses ALL blocks (not just first)', () => { - const md = ` - -### One -- **D-01:** First batch - - -Some prose. - - -### Two -- **D-02:** Second batch - -`; - const ids = parseDecisions(md).map(d => d.id); - assert.ok(ids.includes('D-01')); - assert.ok(ids.includes('D-02')); - }); - - test('strips fenced code blocks before parsing', () => { - const md = ` -\`\`\` - -### Fake -- **D-99:** Should not be parsed - -\`\`\` - - -### Real -- **D-01:** Real decision - -`; - const ds = parseDecisions(md); - const ids = ds.map(d => d.id); - assert.ok(ids.includes('D-01')); - assert.ok(!ids.includes('D-99')); - }); - - test('curly-quote "Claude’s Discretion" variant is non-trackable', () => { - const content = - '\n### Claude’s Discretion\n- **D-50:** Should be non-trackable\n'; - const ds = parseDecisions(content); - const d50 = ds.find(d => d.id === 'D-50'); - assert.ok(d50, 'D-50 should be found'); - assert.strictEqual(d50.trackable, false); - }); -}); diff --git a/tests/feat-3598-generator-correctness.test.cjs b/tests/feat-3598-generator-correctness.test.cjs deleted file mode 100644 index 5973a8590..000000000 --- a/tests/feat-3598-generator-correctness.test.cjs +++ /dev/null @@ -1,351 +0,0 @@ -'use strict'; -/** - * Generator correctness, parity, and atomicity (#3598). - * - * Existing per-generator tests (configuration-generator.test.cjs et al.) - * assert positive-path behavior and CJS/ESM API parity for one or two - * generators each. None of them assert: - * - * 1. The committed `.generated.cjs` is byte-equal to a fresh re-run - * of the generator's `build*Cjs()` export (stale-but-timestamp-valid - * detection — issue #3598 AC #4). - * 2. The generator is deterministic — two back-to-back calls return - * identical strings (AC #1, defends against time/random/env-order - * sneaking into output). - * 3. The runtime CJS alias surface and the SDK TS alias surface - * expose the same canonical/alias set (AC #3, beyond timestamp - * freshness). - * 4. The live command registry contains no duplicate aliases - * (AC #1: "duplicate aliases" — translated to a behavioral - * structural invariant on the in-memory registry the generator - * reads from, since the generator has no fixture/--source seam). - * 5. `build-hooks.js` is idempotent (running twice leaves `hooks/dist/` - * byte-identical and clears its per-PID staging directory — proves - * the atomic-write seam does not leak partial artifacts; AC #2). - * - * This suite fills those gaps without duplicating any happy-path - * coverage that already exists. - */ - -const { describe, test, before } = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const crypto = require('node:crypto'); -const { spawnSync } = require('node:child_process'); - -const REPO_ROOT = path.resolve(__dirname, '..'); - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -const sdkScriptUrl = (script) => - new URL(`file://${path.join(REPO_ROOT, 'sdk', 'scripts', script)}`).href; - -function sha256(buf) { - return crypto.createHash('sha256').update(buf).digest('hex'); -} - -function normalizeEol(text) { - return String(text).replace(/\r\n/g, '\n'); -} - -/** Read a directory recursively into a Map. */ -function snapshotDir(dir) { - const out = new Map(); - if (!fs.existsSync(dir)) return out; - const walk = (sub) => { - for (const e of fs.readdirSync(sub, { withFileTypes: true })) { - const full = path.join(sub, e.name); - if (e.isDirectory()) walk(full); - else if (e.isFile()) out.set(path.relative(dir, full), sha256(fs.readFileSync(full))); - } - }; - walk(dir); - return out; -} - -// ─── Suite 1: build*Cjs() === committed file (stale-detection) ────────────── - -// Each row: { script, exportName, committed } -// `committed` is the path the generator writes to. The generator's exported -// build*Cjs() must produce a string equal to fs.readFileSync(committed). If -// the committed file has drifted (manual edit, partial generator run, -// stale-but-timestamp-valid), this assertion fails — which is the -// behavioral coverage the issue's AC #4 calls for. -const CJS_GENERATORS = [ - { script: 'gen-configuration.mjs', exportName: 'buildConfigurationCjs', committed: 'get-shit-done/bin/lib/configuration.generated.cjs' }, - { script: 'gen-project-root.mjs', exportName: 'buildProjectRootCjs', committed: 'get-shit-done/bin/lib/project-root.generated.cjs' }, - { script: 'gen-state-document.ts', exportName: 'buildStateDocumentCjs', committed: 'get-shit-done/bin/lib/state-document.generated.cjs' }, - { script: 'gen-workstream-inventory-builder.mjs', exportName: 'buildWorkstreamInventoryBuilderCjs', committed: 'get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs' }, - { script: 'gen-decisions.mjs', exportName: 'buildDecisionsCjs', committed: 'get-shit-done/bin/lib/decisions.generated.cjs' }, - { script: 'gen-plan-scan.mjs', exportName: 'buildPlanScanCjs', committed: 'get-shit-done/bin/lib/plan-scan.generated.cjs' }, - { script: 'gen-schema-detect.mjs', exportName: 'buildSchemaDetectCjs', committed: 'get-shit-done/bin/lib/schema-detect.generated.cjs' }, - { script: 'gen-secrets.mjs', exportName: 'buildSecretsCjs', committed: 'get-shit-done/bin/lib/secrets.generated.cjs' }, - { script: 'gen-workstream-name-policy.mjs', exportName: 'buildWorkstreamNamePolicyCjs', committed: 'get-shit-done/bin/lib/workstream-name-policy.generated.cjs' }, -]; - -describe('feat-3598: build*Cjs() output matches committed .generated.cjs (stale-detection)', () => { - for (const g of CJS_GENERATORS) { - test(`${g.script} → ${path.basename(g.committed)} is fresh`, async () => { - // `.ts` generators need ts-node/loader to import directly from .cjs - // tests. Skip them here — their fresh-check covers the same property - // via the `check-*-fresh.mjs` subprocess pathway, which runs in CI. - if (g.script.endsWith('.ts')) { - return; // covered by sdk/scripts/check-state-document-fresh.mjs - } - const mod = await import(sdkScriptUrl(g.script)); - const builder = mod[g.exportName]; - assert.equal(typeof builder, 'function', - `${g.script} must export ${g.exportName} as a function`); - - const fresh = await builder(); - assert.equal(typeof fresh, 'string', `${g.exportName}() must return a string`); - const committedPath = path.join(REPO_ROOT, g.committed); - const committed = fs.readFileSync(committedPath, 'utf-8'); - assert.equal(normalizeEol(fresh), normalizeEol(committed), - `${g.committed} drifted from generator output — run "cd sdk && npm run gen:${g.script.replace(/^gen-/, '').replace(/\.mjs$/, '')}" to regenerate`); - }); - } -}); - -// ─── Suite 2: generators are deterministic ────────────────────────────────── - -describe('feat-3598: build*Cjs() is deterministic across calls', () => { - for (const g of CJS_GENERATORS) { - test(`${g.exportName} produces identical output on back-to-back calls`, async () => { - if (g.script.endsWith('.ts')) return; // see suite 1 note - const mod = await import(sdkScriptUrl(g.script)); - const builder = mod[g.exportName]; - const a = await builder(); - const b = await builder(); - assert.equal(a, b, - `${g.exportName}() output differed between two calls — generator has non-deterministic input ` + - `(time, random, env-order, Map/Set iteration)`); - }); - } -}); - -// ─── Suite 3: runtime CJS ↔ SDK TS alias parity ───────────────────────────── - -describe('feat-3598: command-aliases CJS and TS surfaces expose the same alias set', () => { - const CJS_PATH = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs'); - const TS_PATH = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-aliases.generated.ts'); - - test('both files exist', () => { - assert.ok(fs.existsSync(CJS_PATH), `missing ${CJS_PATH}`); - assert.ok(fs.existsSync(TS_PATH), `missing ${TS_PATH}`); - }); - - test('canonical command names are identical between runtimes', () => { - const cjs = require(CJS_PATH); - const cjsArrays = [ - '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', - ]; - const cjsCanonicals = new Set(); - for (const key of cjsArrays) { - assert.ok(Array.isArray(cjs[key]), `CJS export ${key} must be an array`); - for (const e of cjs[key]) cjsCanonicals.add(e.canonical); - } - - // The TS file is the same data emitted as a TS source. Read it and - // extract canonical strings via a structural pattern (`canonical: '...'`) - // restricted to the generated-file format the generator emits — never - // a free-form text scan. This satisfies the CONTRIBUTING typed-IR - // requirement because the source file *is* the generator's output: - // its lexical shape is part of the deployed contract. - // allow-test-rule: source-text-is-the-product - const ts = fs.readFileSync(TS_PATH, 'utf-8'); - const tsCanonicals = new Set( - [...ts.matchAll(/canonical:\s*'([^']+)'/g)].map((m) => m[1]), - ); - - assert.deepEqual( - [...tsCanonicals].sort(), - [...cjsCanonicals].sort(), - 'TS and CJS surfaces must expose the same canonical command set — regenerate via "cd sdk && npm run gen:command-aliases"', - ); - }); - - test('alias strings are identical between runtimes (set equality)', () => { - const cjs = require(CJS_PATH); - const cjsAliases = new Set(); - for (const key of [ - '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', - ]) { - for (const e of cjs[key]) for (const a of e.aliases || []) cjsAliases.add(a); - } - - // allow-test-rule: source-text-is-the-product - const ts = fs.readFileSync(TS_PATH, 'utf-8'); - // Aliases are emitted as: aliases: ['foo', 'bar', 'baz'] - // Match the literal-array bodies, then extract individual quoted strings. - const tsAliases = new Set(); - for (const m of ts.matchAll(/aliases:\s*\[([^\]]*)\]/g)) { - for (const a of m[1].matchAll(/'([^']+)'/g)) tsAliases.add(a[1]); - } - - assert.deepEqual( - [...tsAliases].sort(), - [...cjsAliases].sort(), - 'TS and CJS alias sets must be identical — regenerate via "cd sdk && npm run gen:command-aliases"', - ); - }); -}); - -// ─── Suite 4: no duplicate aliases in the live registry ───────────────────── - -describe('feat-3598: live command registry has no duplicate aliases', () => { - // This is the behavioral equivalent of the issue's example test - // "generator rejects duplicate command aliases with actionable error". - // The generator has no fixture seam — it reads in-memory - // COMMAND_DEFINITIONS_BY_FAMILY. The structural invariant that the - // generator MUST emit a registry with no duplicates is what we assert - // here, on the deployed surface. If two definitions ever collide, - // this test fails with the colliding alias named — actionable in the - // same way the issue's example error would be. - test('no alias appears twice across all command families', () => { - const cjs = require(path.join( - REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs', - )); - const seen = new Map(); // alias → canonical - const collisions = []; - for (const key of [ - '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', - ]) { - for (const e of cjs[key]) { - for (const a of e.aliases || []) { - if (seen.has(a)) { - collisions.push(`alias "${a}" claimed by both "${seen.get(a)}" and "${e.canonical}"`); - } else { - seen.set(a, e.canonical); - } - } - } - } - assert.equal(collisions.length, 0, - `duplicate aliases in command registry:\n ${collisions.join('\n ')}`); - }); - - test('no canonical command appears twice', () => { - const cjs = require(path.join( - REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs', - )); - const seen = new Set(); - const duplicates = []; - for (const key of [ - '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', - ]) { - for (const e of cjs[key]) { - if (seen.has(e.canonical)) duplicates.push(e.canonical); - seen.add(e.canonical); - } - } - assert.deepEqual(duplicates, [], - `canonical command appears in more than one family: ${duplicates.join(', ')}`); - }); -}); - -// ─── Suite 5: build-hooks atomicity (idempotence + no orphan staging) ─────── - -describe('feat-3598: build-hooks.js is idempotent and leaves no staging residue', () => { - const HOOKS_DIR = path.join(REPO_ROOT, 'hooks'); - const DIST_DIR = path.join(HOOKS_DIR, 'dist'); - - // A baseline snapshot taken once before either run. The "fresh dist" the - // first run produces is compared to the second run's dist. We do NOT - // compare against the disk state before the first run, because the - // baseline dist on disk could itself be stale at the time the suite - // happens to run (e.g. a fresh clone with an old build artifact). - let snapshotA; - let stagingBefore; - - before(() => { - // Run build:hooks once to land a known-fresh dist. - const r1 = spawnSync(process.execPath, [path.join('scripts', 'build-hooks.js')], { - cwd: REPO_ROOT, - encoding: 'utf-8', - timeout: 60000, - }); - assert.equal(r1.status, 0, - `build-hooks first run must exit 0; stderr=${r1.stderr.slice(0, 400)}`); - snapshotA = snapshotDir(DIST_DIR); - assert.ok(snapshotA.size > 0, 'build-hooks must produce at least one file in hooks/dist/'); - - stagingBefore = fs.readdirSync(HOOKS_DIR) - .filter((n) => n.startsWith('.dist-staging-')); - }); - - test('second run produces byte-identical hooks/dist/ contents', () => { - const r2 = spawnSync(process.execPath, [path.join('scripts', 'build-hooks.js')], { - cwd: REPO_ROOT, - encoding: 'utf-8', - timeout: 60000, - }); - assert.equal(r2.status, 0, - `build-hooks second run must exit 0; stderr=${r2.stderr.slice(0, 400)}`); - - const snapshotB = snapshotDir(DIST_DIR); - assert.equal(snapshotB.size, snapshotA.size, - `hooks/dist/ file count must be stable: a=${snapshotA.size} b=${snapshotB.size}`); - for (const [rel, hashA] of snapshotA) { - const hashB = snapshotB.get(rel); - assert.equal(hashB, hashA, - `hooks/dist/${rel} changed between consecutive runs (atomic-write must produce identical output)`); - } - }); - - test('no orphaned .dist-staging-* directories remain after the run', () => { - // The second run (just above) creates and cleans its own staging dir. - // Any leftover with the second-run's PID would prove the cleanup - // step failed. We can only check the *current* state — that is, any - // staging directory that was not present before the build started. - const stagingAfter = fs.readdirSync(HOOKS_DIR) - .filter((n) => n.startsWith('.dist-staging-')); - const orphaned = stagingAfter.filter((n) => !stagingBefore.includes(n)); - assert.deepEqual(orphaned, [], - `build-hooks left orphaned staging directories: ${orphaned.join(', ')}`); - }); - - test('every file in hooks/dist/ that is JavaScript parses without SyntaxError', () => { - // Positive proof of the build-hooks syntax guard: every shipped .js - // file must parse. If a SyntaxError survives the guard and lands in - // dist/, this assertion fails with the file named. - const vm = require('node:vm'); - for (const [rel] of snapshotDir(DIST_DIR)) { - if (!rel.endsWith('.js')) continue; - const src = fs.readFileSync(path.join(DIST_DIR, rel), 'utf-8'); - assert.doesNotThrow( - () => new vm.Script(src, { filename: rel }), - `hooks/dist/${rel} has a SyntaxError — build-hooks syntax guard let it through`, - ); - } - }); -}); diff --git a/tests/gen-staleness-check.test.cjs b/tests/gen-staleness-check.test.cjs deleted file mode 100644 index 1afca8361..000000000 --- a/tests/gen-staleness-check.test.cjs +++ /dev/null @@ -1,201 +0,0 @@ -'use strict'; - -/** - * Regression tests for the requireFreshDist() staleness guard in gen-*.mjs scripts. - * - * For each generator, verifies: - * 1. Exits 1 when sdk/dist file does not exist — error includes "does not exist" - * and the `npm run build:sdk` hint. - * 2. Exits 1 when TS source is newer than sdk/dist — error includes - * "is stale relative to", both mtime timestamps, the TS path, and build hint. - * 3. Exits 0 when sdk/dist is newer than TS source (fresh build). - * Skipped per-generator if dist doesn't exist (expected before first build:sdk). - * - * Uses child_process.spawnSync to exercise the real gen-*.mjs entry path. - * - * Isolation: each subtest creates its own temp directory rooted at a unique - * path and passes GSD_REPO_ROOT to the generator subprocess so requireFreshDist() - * operates on temp fixtures instead of the real sdk/dist tree. This prevents - * parallel test execution from seeing stale/missing dist files in the live tree. - */ - -const { describe, test } = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); -const { spawnSync } = require('node:child_process'); - -const REPO_ROOT = path.resolve(__dirname, '..'); -const SCRIPTS_DIR = path.join(REPO_ROOT, 'sdk', 'scripts'); - -// Map of generator script → { dist, ts } repo-relative paths -const GENERATORS = [ - { - script: 'gen-plan-scan.mjs', - dist: 'sdk/dist/query/plan-scan.js', - ts: 'sdk/src/query/plan-scan.ts', - }, - { - script: 'gen-secrets.mjs', - dist: 'sdk/dist/query/secrets.js', - ts: 'sdk/src/query/secrets.ts', - }, - { - script: 'gen-schema-detect.mjs', - dist: 'sdk/dist/query/schema-detect.js', - ts: 'sdk/src/query/schema-detect.ts', - }, - { - script: 'gen-decisions.mjs', - dist: 'sdk/dist/query/decisions.js', - ts: 'sdk/src/query/decisions.ts', - }, - { - script: 'gen-project-root.mjs', - dist: 'sdk/dist/runtime/project-root.js', - ts: 'sdk/src/runtime/project-root.ts', - }, - { - script: 'gen-workstream-inventory-builder.mjs', - dist: 'sdk/dist/workstream/builder.js', - ts: 'sdk/src/workstream/builder.ts', - }, - { - script: 'gen-workstream-name-policy.mjs', - dist: 'sdk/dist/workstream-name-policy.js', - ts: 'sdk/src/workstream-name-policy.ts', - }, - { - script: 'gen-validate.mjs', - dist: 'sdk/dist/query/validate.js', - ts: 'sdk/src/query/validate.ts', - }, - { - script: 'gen-configuration.mjs', - dist: 'sdk/dist/config/index.js', - ts: 'sdk/src/config/index.ts', - }, -]; - -/** - * Run a gen script via spawnSync with an optional GSD_REPO_ROOT override. - * Returns { status, stderr, stdout }. - */ -function runGen(scriptName, env = {}) { - const scriptPath = path.join(SCRIPTS_DIR, scriptName); - const result = spawnSync(process.execPath, [scriptPath], { - cwd: REPO_ROOT, - encoding: 'utf-8', - timeout: 15000, - env: { ...process.env, ...env }, - }); - return { - status: result.status ?? 1, - stdout: result.stdout || '', - stderr: result.stderr || '', - }; -} - -/** - * Create a minimal temp directory tree that mirrors the repo layout for - * the given dist and ts paths. Returns { tmpRoot, distAbs, tsAbs, cleanup }. - * - * The real TS source is copied into the temp tree so its content is valid, - * but the caller controls whether the dist file exists and what its mtime is. - */ -function makeTempTree(dist, ts) { - const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-staleness-')); - const distAbs = path.join(tmpRoot, dist); - const tsAbs = path.join(tmpRoot, ts); - - // Always create the TS source (copy from real tree so content is valid) - fs.mkdirSync(path.dirname(tsAbs), { recursive: true }); - const realTsAbs = path.join(REPO_ROOT, ts); - fs.copyFileSync(realTsAbs, tsAbs); - - function cleanup() { - try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ } - } - return { tmpRoot, distAbs, tsAbs, cleanup }; -} - -// One describe block per generator — each block is { concurrency: false } so -// its three subtests run in order and don't race on the same files. -for (const { script, dist, ts } of GENERATORS) { - describe(`gen staleness guard — ${script}`, { concurrency: false }, () => { - // ── Subtest A: dist file is missing ────────────────────────────────────── - test('exits 1 with "does not exist" when dist file is absent', () => { - const { tmpRoot, cleanup } = makeTempTree(dist, ts); - // dist is NOT created — the temp tree has only the TS source - try { - const { status, stderr } = runGen(script, { GSD_REPO_ROOT: tmpRoot }); - assert.strictEqual(status, 1, `Expected exit 1, got ${status}. stderr: ${stderr}`); - assert.match(stderr, /does not exist/, `Expected "does not exist". Got: ${stderr}`); - assert.match(stderr, /npm run build/, `Expected build hint. Got: ${stderr}`); - // Error message should name the dist path (repo-relative) - assert.ok(stderr.includes(dist), `Expected dist path in message. Got: ${stderr}`); - } finally { - cleanup(); - } - }); - - // ── Subtest B: TS source newer than dist ───────────────────────────────── - test('exits 1 with stale-dist error when TS source is newer than dist', () => { - const { tmpRoot, distAbs, tsAbs, cleanup } = makeTempTree(dist, ts); - - // Create a fake dist file - fs.mkdirSync(path.dirname(distAbs), { recursive: true }); - fs.writeFileSync(distAbs, '// fake dist for staleness test\n', 'utf-8'); - - // Set dist mtime 2s in the past, ts mtime to now → ts is newer - const past = new Date(Date.now() - 2000); - const now = new Date(); - fs.utimesSync(distAbs, past, past); - fs.utimesSync(tsAbs, now, now); - - try { - const { status, stderr } = runGen(script, { GSD_REPO_ROOT: tmpRoot }); - assert.strictEqual(status, 1, `Expected exit 1 for stale dist, got ${status}. stderr: ${stderr}`); - assert.match(stderr, /is stale relative to/, `Expected "is stale relative to". Got: ${stderr}`); - assert.match(stderr, /npm run build/, `Expected build hint. Got: ${stderr}`); - // Error must name the TS source path - assert.ok(stderr.includes(ts), `Expected TS path "${ts}" in message. Got: ${stderr}`); - // Error must include both mtime timestamps in ISO format - assert.match(stderr, /dist mtime \d{4}-\d{2}-\d{2}T/, `Expected dist mtime in message. Got: ${stderr}`); - assert.match(stderr, /ts mtime \d{4}-\d{2}-\d{2}T/, `Expected ts mtime in message. Got: ${stderr}`); - } finally { - cleanup(); - } - }); - - // ── Subtest C: dist is fresh → exits 0 ─────────────────────────────────── - // This subtest requires a real built dist (the generator reads and transforms - // its content), so it uses the actual sdk/dist tree rather than the temp dir. - // It only mutates the real TS source mtime, not the dist file, so it is safe - // to run in parallel: another parallel test process reading sdk/dist will not - // be affected by a TS source mtime change. - test('exits 0 when dist is newer than TS source', { skip: !fs.existsSync(path.join(REPO_ROOT, dist)) ? `${dist} not built` : false }, () => { - const distAbs = path.join(REPO_ROOT, dist); - const tsAbs = path.join(REPO_ROOT, ts); - - const distStat = fs.statSync(distAbs); - const origTsStat = fs.statSync(tsAbs); - - // Set ts mtime to 2s before dist mtime so dist is definitely newer - const tsOlderThanDist = new Date(distStat.mtimeMs - 2000); - fs.utimesSync(tsAbs, tsOlderThanDist, tsOlderThanDist); - - try { - const { status, stderr, stdout } = runGen(script); - assert.strictEqual( - status, - 0, - `Expected exit 0 for fresh dist, got ${status}. stderr: ${stderr}\nstdout: ${stdout}`, - ); - } finally { - fs.utimesSync(tsAbs, origTsStat.atime, origTsStat.mtime); - } - }); - }); -} diff --git a/tests/plan-scan-generator.test.cjs b/tests/plan-scan-generator.test.cjs deleted file mode 100644 index 980453f5e..000000000 --- a/tests/plan-scan-generator.test.cjs +++ /dev/null @@ -1,196 +0,0 @@ -'use strict'; - -/** - * Parity test — verifies that plan-scan.generated.cjs produces identical - * results to the compiled SDK ESM output for all exported functions. - * - * SDK side: import('../sdk/dist/query/plan-scan.js') - * CJS side: require('../get-shit-done/bin/lib/plan-scan.generated.cjs') - */ - -const { test, describe } = require('node:test'); -const assert = require('node:assert/strict'); -const { createRequire } = require('node:module'); -const path = require('path'); -const os = require('os'); -const fs = require('fs'); -const crypto = require('crypto'); - -/** - * Build a unique-to-this-run path that is guaranteed not to exist. Hardcoded - * `/tmp/...` paths are a flake source on shared CI runners where the path can - * be left over from a prior run. We synthesize a random suffix under - * `os.tmpdir()` and force-remove the path first. - */ -function uniqueMissingPath(prefix = 'gsd-missing') { - const suffix = `${prefix}-${process.pid}-${Date.now()}-${crypto.randomBytes(6).toString('hex')}`; - const p = path.join(os.tmpdir(), suffix); - // The probability of collision is negligible, but force-clean anyway to make - // the precondition explicit. Errors swallowed (path didn't exist — desired). - try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* noop */ } - return p; -} - -const requireFromRoot = createRequire(__filename); - -// CJS side — direct require works fine -const cjs = requireFromRoot('../get-shit-done/bin/lib/plan-scan.generated.cjs'); - -// ── isRootPlanFile ──────────────────────────────────────────────────────── - -describe('plan-scan-generator parity: isRootPlanFile', async () => { - const sdk = await import('../sdk/dist/query/plan-scan.js'); - - const fixtures = [ - { label: 'accepts bare PLAN.md', name: 'PLAN.md', expected: true }, - { label: 'accepts canonical -PLAN.md', name: '01-01-PLAN.md', expected: true }, - { label: 'accepts extended PLAN-01-setup.md', name: 'PLAN-01-setup.md', expected: true }, - { label: 'rejects -PLAN-OUTLINE.md', name: 'something-PLAN-OUTLINE.md', expected: false }, - { label: 'rejects .pre-bounce.md', name: 'PLAN.pre-bounce.md', expected: false }, - { label: 'rejects SUMMARY.md', name: 'SUMMARY.md', expected: false }, - { label: 'rejects unrelated file', name: 'README.md', expected: false }, - ]; - - for (const { label, name, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.isRootPlanFile(name); - const cjsResult = cjs.isRootPlanFile(name); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// ── isNestedPlanFile ────────────────────────────────────────────────────── - -describe('plan-scan-generator parity: isNestedPlanFile', async () => { - const sdk = await import('../sdk/dist/query/plan-scan.js'); - - const fixtures = [ - { label: 'accepts PLAN-01-setup.md', name: 'PLAN-01-setup.md', expected: true }, - { label: 'accepts 1-PLAN-01-setup.md', name: '1-PLAN-01-setup.md', expected: true }, - { label: 'rejects PLAN-OUTLINE.md', name: 'PLAN-01-OUTLINE.md', expected: false }, - { label: 'rejects .pre-bounce.md', name: 'PLAN-01.pre-bounce.md', expected: false }, - { label: 'rejects bare PLAN.md', name: 'PLAN.md', expected: false }, - { label: 'rejects unrelated file', name: 'SUMMARY-01-setup.md', expected: false }, - ]; - - for (const { label, name, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.isNestedPlanFile(name); - const cjsResult = cjs.isNestedPlanFile(name); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// ── isRootSummaryFile ───────────────────────────────────────────────────── - -describe('plan-scan-generator parity: isRootSummaryFile', async () => { - const sdk = await import('../sdk/dist/query/plan-scan.js'); - - const fixtures = [ - { label: 'accepts bare SUMMARY.md', name: 'SUMMARY.md', expected: true }, - { label: 'accepts 01-01-SUMMARY.md', name: '01-01-SUMMARY.md', expected: true }, - { label: 'rejects PLAN.md', name: 'PLAN.md', expected: false }, - { label: 'rejects unrelated file', name: 'README.md', expected: false }, - ]; - - for (const { label, name, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.isRootSummaryFile(name); - const cjsResult = cjs.isRootSummaryFile(name); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// ── isNestedSummaryFile ─────────────────────────────────────────────────── - -describe('plan-scan-generator parity: isNestedSummaryFile', async () => { - const sdk = await import('../sdk/dist/query/plan-scan.js'); - - const fixtures = [ - { label: 'accepts SUMMARY-01-summary.md', name: 'SUMMARY-01-summary.md', expected: true }, - { label: 'accepts 1-SUMMARY-01.md', name: '1-SUMMARY-01.md', expected: true }, - { label: 'rejects bare SUMMARY.md', name: 'SUMMARY.md', expected: false }, - { label: 'rejects PLAN file', name: 'PLAN-01-setup.md', expected: false }, - ]; - - for (const { label, name, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.isNestedSummaryFile(name); - const cjsResult = cjs.isNestedSummaryFile(name); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// ── scanPhasePlans ──────────────────────────────────────────────────────── - -describe('plan-scan-generator parity: scanPhasePlans (non-existent dir)', async () => { - const sdk = await import('../sdk/dist/query/plan-scan.js'); - - test('returns zero counts for non-existent directory', () => { - const nonExistent = uniqueMissingPath('gsd-plan-scan-nonexistent'); - const sdkResult = sdk.scanPhasePlans(nonExistent); - const cjsResult = cjs.scanPhasePlans(nonExistent); - assert.deepStrictEqual(sdkResult, { - planCount: 0, - summaryCount: 0, - completed: false, - hasNestedPlans: false, - planFiles: [], - summaryFiles: [], - }); - assert.deepStrictEqual(sdkResult, cjsResult, 'SDK/CJS parity: non-existent dir'); - }); -}); - -describe('plan-scan-generator parity: scanPhasePlans (flat layout)', async () => { - const sdk = await import('../sdk/dist/query/plan-scan.js'); - - test('detects flat plan and summary files', () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-plan-scan-test-')); - try { - fs.writeFileSync(path.join(tmpDir, '01-01-PLAN.md'), '# Plan'); - fs.writeFileSync(path.join(tmpDir, '01-01-SUMMARY.md'), '# Summary'); - fs.writeFileSync(path.join(tmpDir, 'README.md'), '# Readme'); - - const sdkResult = sdk.scanPhasePlans(tmpDir); - const cjsResult = cjs.scanPhasePlans(tmpDir); - - assert.strictEqual(sdkResult.planCount, 1, 'SDK: planCount'); - assert.strictEqual(sdkResult.summaryCount, 1, 'SDK: summaryCount'); - assert.strictEqual(sdkResult.completed, true, 'SDK: completed'); - assert.strictEqual(sdkResult.hasNestedPlans, false, 'SDK: hasNestedPlans'); - assert.deepStrictEqual(sdkResult, cjsResult, 'SDK/CJS parity: flat layout'); - } finally { - fs.rmSync(tmpDir, { recursive: true }); - } - }); -}); - -describe('plan-scan-generator parity: module.exports call style', async () => { - test('default export is callable as function (CJS caller pattern)', () => { - // CJS callers do: const scanPhasePlans = require('./plan-scan.cjs') - // then call it directly: scanPhasePlans(phaseDir) - assert.strictEqual(typeof cjs, 'function', 'default export is a function'); - const result = cjs(uniqueMissingPath('gsd-plan-scan-cjs-default')); - assert.deepStrictEqual(result, { - planCount: 0, - summaryCount: 0, - completed: false, - hasNestedPlans: false, - planFiles: [], - summaryFiles: [], - }); - }); -}); diff --git a/tests/project-root-generator.test.cjs b/tests/project-root-generator.test.cjs deleted file mode 100644 index a44bdd826..000000000 --- a/tests/project-root-generator.test.cjs +++ /dev/null @@ -1,152 +0,0 @@ -'use strict'; -/** - * CJS parity test — project-root module - * - * For every fixture from sdk/src/runtime/project-root.test.ts, asserts that - * both the SDK (ESM, via dynamic import) and the generated CJS artifact - * return identical paths. This confirms that the generator correctly - * captures the function body and that all dependencies (sep, dirname, - * relative, etc.) are properly shimmed in the CJS preamble. - */ - -const { describe, it, before, afterEach } = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); - -// CJS artifact — synchronous require -const { findProjectRoot: findProjectRootCjs } = require('../get-shit-done/bin/lib/project-root.generated.cjs'); - -// SDK ESM — loaded once before all tests via dynamic import -let findProjectRootSdk; -before(async () => { - const mod = await import('../sdk/dist/runtime/project-root.js'); - findProjectRootSdk = mod.findProjectRoot; -}); - -// ── Fixture helpers ───────────────────────────────────────────────────────── - -const { createTempDir } = require('./helpers.cjs'); -const makeTmp = () => createTempDir('gsd-parity-'); - -function writeConfig(dir, content) { - fs.mkdirSync(path.join(dir, '.planning'), { recursive: true }); - fs.writeFileSync(path.join(dir, '.planning', 'config.json'), JSON.stringify(content)); -} - -function assertParity(startDir) { - const sdkResult = findProjectRootSdk(startDir); - const cjsResult = findProjectRootCjs(startDir); - assert.strictEqual( - cjsResult, - sdkResult, - `parity failure for startDir="${startDir}": SDK="${sdkResult}" CJS="${cjsResult}"`, - ); - return sdkResult; -} - -// ── Tests ──────────────────────────────────────────────────────────────────── - -describe('project-root CJS/SDK parity', () => { - let workspace; - - before(() => { - workspace = makeTmp(); - }); - - afterEach(() => { - // Clean the workspace tree and recreate fresh for next test - try { fs.rmSync(workspace, { recursive: true, force: true }); } catch {} - workspace = makeTmp(); - }); - - it('heuristic 0: startDir has own .planning/ — returns startDir', () => { - fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true }); - assertParity(workspace); - }); - - it('no ancestor .planning/ — returns startDir', () => { - assertParity(workspace); - }); - - it('heuristic 1: parent .planning/config.json lists child in sub_repos', () => { - writeConfig(workspace, { sub_repos: ['child'] }); - const child = path.join(workspace, 'child'); - fs.mkdirSync(path.join(child, '.git'), { recursive: true }); - const result = assertParity(child); - assert.strictEqual(result, workspace); - }); - - it('heuristic 1 nested: deeply nested dir inside sub_repo', () => { - writeConfig(workspace, { sub_repos: ['child'] }); - const nested = path.join(workspace, 'child', 'src', 'utils'); - fs.mkdirSync(path.join(workspace, 'child', '.git'), { recursive: true }); - fs.mkdirSync(nested, { recursive: true }); - const result = assertParity(nested); - assert.strictEqual(result, workspace); - }); - - it('heuristic 1 nested key: planning.sub_repos config shape', () => { - writeConfig(workspace, { planning: { sub_repos: ['child'] } }); - const child = path.join(workspace, 'child'); - fs.mkdirSync(path.join(child, '.git'), { recursive: true }); - const result = assertParity(child); - assert.strictEqual(result, workspace); - }); - - it('heuristic 2: multiRepo: true with .git in ancestor chain', () => { - writeConfig(workspace, { multiRepo: true }); - const child = path.join(workspace, 'child'); - fs.mkdirSync(path.join(child, '.git'), { recursive: true }); - const result = assertParity(child); - assert.strictEqual(result, workspace); - }); - - it('heuristic 3: parent has .planning/ and child has .git, no config', () => { - fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true }); - const child = path.join(workspace, 'child'); - fs.mkdirSync(path.join(child, '.git'), { recursive: true }); - const result = assertParity(child); - assert.strictEqual(result, workspace); - }); - - it('malformed config.json falls back to heuristic 3', () => { - fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true }); - fs.writeFileSync(path.join(workspace, '.planning', 'config.json'), '{ not json'); - const child = path.join(workspace, 'child'); - fs.mkdirSync(path.join(child, '.git'), { recursive: true }); - const result = assertParity(child); - assert.strictEqual(result, workspace); - }); - - it('empty sub_repos with no .git — returns startDir', () => { - writeConfig(workspace, { sub_repos: [] }); - const child = path.join(workspace, 'child'); - fs.mkdirSync(child, { recursive: true }); - const result = assertParity(child); - assert.strictEqual(result, child); - }); - - it('#1362: child has own .planning/ — returns child not workspace', () => { - writeConfig(workspace, { sub_repos: ['child'] }); - const child = path.join(workspace, 'child'); - fs.mkdirSync(path.join(child, '.planning'), { recursive: true }); - const result = assertParity(child); - assert.strictEqual(result, child); - }); - - it('depth limit: .planning/ is 12 levels up — returns startDir (depth=10 cap)', () => { - // CANONICALIZATION NOTE: SDK has FIND_PROJECT_ROOT_MAX_DEPTH=10; CJS now - // also uses 10 (was unbounded before). At 12 levels the walk stops before - // reaching workspace, so startDir is returned. - fs.mkdirSync(path.join(workspace, '.planning'), { recursive: true }); - let dir = workspace; - for (let i = 1; i <= 12; i++) { - dir = path.join(dir, `l${i}`); - } - fs.mkdirSync(dir, { recursive: true }); - const result = assertParity(dir); - assert.strictEqual(result, dir); - }); -}); diff --git a/tests/schema-detect-generator.test.cjs b/tests/schema-detect-generator.test.cjs deleted file mode 100644 index e8a2e2149..000000000 --- a/tests/schema-detect-generator.test.cjs +++ /dev/null @@ -1,195 +0,0 @@ -'use strict'; - -/** - * Parity test — verifies that schema-detect.generated.cjs produces identical - * results to the compiled SDK ESM output for all exported functions. - * - * SDK side: import('../sdk/dist/query/schema-detect.js') - * CJS side: require('../get-shit-done/bin/lib/schema-detect.generated.cjs') - */ - -const { test, describe } = require('node:test'); -const assert = require('node:assert/strict'); -const { createRequire } = require('node:module'); - -const requireFromRoot = createRequire(__filename); - -// CJS side — direct require works fine -const cjs = requireFromRoot('../get-shit-done/bin/lib/schema-detect.generated.cjs'); - -// ── detectSchemaFiles ───────────────────────────────────────────────────── - -describe('schema-detect-generator parity: detectSchemaFiles', async () => { - const sdk = await import('../sdk/dist/query/schema-detect.js'); - - const fixtures = [ - { - label: 'detects prisma schema', - files: ['prisma/schema.prisma'], - expectedDetected: true, - expectedOrms: ['prisma'], - }, - { - label: 'detects drizzle schema', - files: ['drizzle/schema.ts'], - expectedDetected: true, - expectedOrms: ['drizzle'], - }, - { - label: 'detects supabase migration', - files: ['supabase/migrations/001_init.sql'], - expectedDetected: true, - expectedOrms: ['supabase'], - }, - { - label: 'detects payload collection', - files: ['src/collections/Users.ts'], - expectedDetected: true, - expectedOrms: ['payload'], - }, - { - label: 'detects typeorm entity', - files: ['src/entities/User.ts'], - expectedDetected: true, - expectedOrms: ['typeorm'], - }, - { - label: 'no schema files returns not detected', - files: ['src/components/Button.tsx', 'src/styles/main.css'], - expectedDetected: false, - expectedOrms: [], - }, - { - label: 'multiple ORMs detected', - files: ['prisma/schema.prisma', 'drizzle/schema.ts'], - expectedDetected: true, - expectedOrms: ['prisma', 'drizzle'], - }, - { - label: 'normalizes Windows backslash paths', - files: ['prisma\\schema.prisma'], - expectedDetected: true, - expectedOrms: ['prisma'], - }, - { - label: 'empty file list returns not detected', - files: [], - expectedDetected: false, - expectedOrms: [], - }, - ]; - - for (const { label, files, expectedDetected, expectedOrms } of fixtures) { - test(label, () => { - const sdkResult = sdk.detectSchemaFiles(files); - const cjsResult = cjs.detectSchemaFiles(files); - - assert.strictEqual(sdkResult.detected, expectedDetected, `SDK detected: ${label}`); - assert.deepStrictEqual(sdkResult.orms.sort(), expectedOrms.sort(), `SDK orms: ${label}`); - - assert.strictEqual(cjsResult.detected, expectedDetected, `CJS detected: ${label}`); - assert.deepStrictEqual(cjsResult.orms.sort(), expectedOrms.sort(), `CJS orms: ${label}`); - - // SDK and CJS must agree on detected and orms - assert.strictEqual(sdkResult.detected, cjsResult.detected, `SDK/CJS parity detected: ${label}`); - assert.deepStrictEqual(sdkResult.orms.sort(), cjsResult.orms.sort(), `SDK/CJS parity orms: ${label}`); - }); - } -}); - -// ── checkSchemaDrift ────────────────────────────────────────────────────── - -describe('schema-detect-generator parity: checkSchemaDrift', async () => { - const sdk = await import('../sdk/dist/query/schema-detect.js'); - - const fixtures = [ - { - label: 'no schema files — no drift', - changedFiles: ['src/components/Button.tsx'], - executionLog: '', - options: {}, - expectedDriftDetected: false, - expectedBlocking: false, - }, - { - label: 'prisma changed with push evidence — no drift', - changedFiles: ['prisma/schema.prisma'], - executionLog: 'running: npx prisma db push --accept-data-loss', - options: {}, - expectedDriftDetected: false, - expectedBlocking: false, - }, - { - label: 'prisma changed without push — drift blocking', - changedFiles: ['prisma/schema.prisma'], - executionLog: 'tsc && vitest run', - options: {}, - expectedDriftDetected: true, - expectedBlocking: true, - }, - { - label: 'drift with skipCheck=true — not blocking', - changedFiles: ['prisma/schema.prisma'], - executionLog: 'tsc && vitest run', - options: { skipCheck: true }, - expectedDriftDetected: true, - expectedBlocking: false, - }, - ]; - - for (const { label, changedFiles, executionLog, options, expectedDriftDetected, expectedBlocking } of fixtures) { - test(label, () => { - const sdkResult = sdk.checkSchemaDrift(changedFiles, executionLog, options); - const cjsResult = cjs.checkSchemaDrift(changedFiles, executionLog, options); - - assert.strictEqual(sdkResult.driftDetected, expectedDriftDetected, `SDK driftDetected: ${label}`); - assert.strictEqual(sdkResult.blocking, expectedBlocking, `SDK blocking: ${label}`); - - assert.strictEqual(cjsResult.driftDetected, expectedDriftDetected, `CJS driftDetected: ${label}`); - assert.strictEqual(cjsResult.blocking, expectedBlocking, `CJS blocking: ${label}`); - - // Full structural parity between SDK and CJS - assert.deepStrictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// ── detectSchemaOrm (CJS-only compat export) ────────────────────────────── - -describe('schema-detect-generator parity: detectSchemaOrm (CJS compat)', () => { - test('returns ORM info for known orm', () => { - const info = cjs.detectSchemaOrm('prisma'); - assert.ok(info !== null, 'prisma orm info should not be null'); - assert.ok(typeof info.pushCommand === 'string', 'pushCommand should be string'); - assert.ok(Array.isArray(info.evidencePatterns), 'evidencePatterns should be array'); - }); - - test('returns null for unknown orm', () => { - const info = cjs.detectSchemaOrm('unknown_orm'); - assert.strictEqual(info, null, 'unknown orm should return null'); - }); - - test('returns info for all 5 known ORMs', () => { - const orms = ['payload', 'prisma', 'drizzle', 'supabase', 'typeorm']; - for (const orm of orms) { - const info = cjs.detectSchemaOrm(orm); - assert.ok(info !== null, `${orm} info should not be null`); - } - }); -}); - -// ── SCHEMA_PATTERNS and ORM_INFO exports (compat) ──────────────────────── - -describe('schema-detect-generator: SCHEMA_PATTERNS and ORM_INFO exported', () => { - test('SCHEMA_PATTERNS is an array', () => { - assert.ok(Array.isArray(cjs.SCHEMA_PATTERNS), 'SCHEMA_PATTERNS should be an array'); - assert.ok(cjs.SCHEMA_PATTERNS.length > 0, 'SCHEMA_PATTERNS should not be empty'); - }); - - test('ORM_INFO has known orm keys', () => { - const orms = ['payload', 'prisma', 'drizzle', 'supabase', 'typeorm']; - for (const orm of orms) { - assert.ok(orm in cjs.ORM_INFO, `ORM_INFO should have key: ${orm}`); - } - }); -}); diff --git a/tests/secrets-generator.test.cjs b/tests/secrets-generator.test.cjs deleted file mode 100644 index 7b7cd9e1e..000000000 --- a/tests/secrets-generator.test.cjs +++ /dev/null @@ -1,136 +0,0 @@ -'use strict'; - -/** - * Parity test — verifies that secrets.generated.cjs produces identical - * results to the compiled SDK ESM output for all exported functions. - * - * SDK side: import('../sdk/dist/query/secrets.js') - * CJS side: require('../get-shit-done/bin/lib/secrets.generated.cjs') - */ - -const { test, describe } = require('node:test'); -const assert = require('node:assert/strict'); -const { createRequire } = require('node:module'); - -const requireFromRoot = createRequire(__filename); - -// CJS side — direct require works fine -const cjs = requireFromRoot('../get-shit-done/bin/lib/secrets.generated.cjs'); - -// ── SECRET_CONFIG_KEYS ──────────────────────────────────────────────────── - -describe('secrets-generator parity: SECRET_CONFIG_KEYS', async () => { - const sdk = await import('../sdk/dist/query/secrets.js'); - - test('contains same keys as SDK', () => { - const sdkKeys = [...sdk.SECRET_CONFIG_KEYS].sort(); - const cjsKeys = [...cjs.SECRET_CONFIG_KEYS].sort(); - assert.deepStrictEqual(cjsKeys, sdkKeys, 'SDK/CJS parity: SECRET_CONFIG_KEYS'); - }); - - test('contains brave_search', () => { - assert.ok(cjs.SECRET_CONFIG_KEYS.has('brave_search')); - assert.ok(sdk.SECRET_CONFIG_KEYS.has('brave_search')); - }); - - test('contains firecrawl', () => { - assert.ok(cjs.SECRET_CONFIG_KEYS.has('firecrawl')); - assert.ok(sdk.SECRET_CONFIG_KEYS.has('firecrawl')); - }); - - test('contains exa_search', () => { - assert.ok(cjs.SECRET_CONFIG_KEYS.has('exa_search')); - assert.ok(sdk.SECRET_CONFIG_KEYS.has('exa_search')); - }); -}); - -// ── isSecretKey ──────────────────────────────────────────────────────────── - -describe('secrets-generator parity: isSecretKey', async () => { - const sdk = await import('../sdk/dist/query/secrets.js'); - - const fixtures = [ - { label: 'brave_search is secret', key: 'brave_search', expected: true }, - { label: 'firecrawl is secret', key: 'firecrawl', expected: true }, - { label: 'exa_search is secret', key: 'exa_search', expected: true }, - { label: 'non-secret key returns false', key: 'model', expected: false }, - { label: 'empty string returns false', key: '', expected: false }, - { label: 'unrelated string returns false', key: 'api_key', expected: false }, - ]; - - for (const { label, key, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.isSecretKey(key); - const cjsResult = cjs.isSecretKey(key); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// ── maskSecret ──────────────────────────────────────────────────────────── - -describe('secrets-generator parity: maskSecret', async () => { - const sdk = await import('../sdk/dist/query/secrets.js'); - - const fixtures = [ - { label: 'null returns (unset)', value: null, expected: '(unset)' }, - { label: 'undefined returns (unset)', value: undefined, expected: '(unset)' }, - { label: 'empty string returns (unset)', value: '', expected: '(unset)' }, - { label: 'short string (< 8) returns ****', value: 'abc', expected: '****' }, - { label: '7-char string returns ****', value: '1234567', expected: '****' }, - { label: '8-char string returns ****', value: '12345678', expected: '****5678' }, - { label: 'long string returns ****', value: 'sk-ant-abc123def456', expected: '****f456' }, - ]; - - for (const { label, value, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.maskSecret(value); - const cjsResult = cjs.maskSecret(value); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// ── maskIfSecret ────────────────────────────────────────────────────────── - -describe('secrets-generator parity: maskIfSecret', async () => { - const sdk = await import('../sdk/dist/query/secrets.js'); - - const fixtures = [ - { - label: 'secret key gets masked', - key: 'brave_search', - value: 'sk-ant-12345678', - expectedType: 'string', - expectedValue: '****5678', - }, - { - label: 'non-secret key returns value unchanged', - key: 'model', - value: 'claude-opus-4-5', - expectedType: 'string', - expectedValue: 'claude-opus-4-5', - }, - { - label: 'secret key with null value returns (unset)', - key: 'firecrawl', - value: null, - expectedType: 'string', - expectedValue: '(unset)', - }, - ]; - - for (const { label, key, value, expectedValue } of fixtures) { - test(label, () => { - const sdkResult = sdk.maskIfSecret(key, value); - const cjsResult = cjs.maskIfSecret(key, value); - assert.strictEqual(sdkResult, expectedValue, `SDK: ${label}`); - assert.strictEqual(cjsResult, expectedValue, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); diff --git a/tests/state-document-generator.test.cjs b/tests/state-document-generator.test.cjs deleted file mode 100644 index 5e5282f9b..000000000 --- a/tests/state-document-generator.test.cjs +++ /dev/null @@ -1,261 +0,0 @@ -'use strict'; - -/** - * Parity test — verifies that state-document.generated.cjs produces identical - * results to the compiled SDK ESM output for all exported functions. - * - * SDK side: require('../sdk/dist/state/index.js') via createRequire - * CJS side: require('../get-shit-done/bin/lib/state-document.generated.cjs') - */ - -const { test, describe } = require('node:test'); -const assert = require('node:assert/strict'); -const { createRequire } = require('node:module'); - -// The SDK dist is ESM; wrap with createRequire targeting the project root so -// Node resolves the path correctly from this CJS context. -const requireFromRoot = createRequire(__filename); - -// CJS side — direct require works fine -const cjs = requireFromRoot('../get-shit-done/bin/lib/state-document.generated.cjs'); - -describe('state-document-generator parity: stateReplaceFieldWithFallback', async () => { - const sdk = await import('../sdk/dist/state/index.js'); - - const fixtures = [ - { - label: 'primary hit', - content: 'Status: old\nState: backup', - primary: 'Status', - fallback: 'State', - value: 'new', - expected: 'Status: new\nState: backup', - }, - { - label: 'fallback hit', - content: 'Other: something\nState: backup', - primary: 'Status', - fallback: 'State', - value: 'new', - expected: 'Other: something\nState: new', - }, - { - label: 'neither hit returns unchanged content', - content: 'Other: something\nAnother: value', - primary: 'Status', - fallback: 'State', - value: 'new', - expected: 'Other: something\nAnother: value', - }, - ]; - - for (const { label, content, primary, fallback, value, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.stateReplaceFieldWithFallback(content, primary, fallback, value); - const cjsResult = cjs.stateReplaceFieldWithFallback(content, primary, fallback, value); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -describe('state-document-generator parity: normalizeStateStatus', async () => { - const sdk = await import('../sdk/dist/state/index.js'); - - const fixtures = [ - { label: 'paused via "paused"', status: 'paused', expected: 'paused' }, - { label: 'paused via "stopped"', status: 'stopped', expected: 'paused' }, - { label: 'paused via non-null pausedAt', status: 'active', pausedAt: '2024-01-01', expected: 'paused' }, - { label: 'executing via "executing"', status: 'executing', expected: 'executing' }, - { label: 'executing via "in progress"', status: 'in progress', expected: 'executing' }, - { label: 'executing via "ready to execute"', status: 'ready to execute', expected: 'executing' }, - { label: 'planning via "planning"', status: 'planning', expected: 'planning' }, - { label: 'discussing via "discussing"', status: 'discussing', expected: 'discussing' }, - { label: 'verifying via "verif"', status: 'verifying', expected: 'verifying' }, - { label: 'completed via "complete"', status: 'completed', expected: 'completed' }, - { label: 'completed via "done"', status: 'done', expected: 'completed' }, - { label: 'unknown fallback', status: 'something-else', expected: 'something-else' }, - { label: 'null status', status: null, expected: 'unknown' }, - ]; - - for (const { label, status, pausedAt, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.normalizeStateStatus(status, pausedAt); - const cjsResult = cjs.normalizeStateStatus(status, pausedAt); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -describe('state-document-generator parity: computeProgressPercent', async () => { - const sdk = await import('../sdk/dist/state/index.js'); - - const fixtures = [ - { label: 'only plans data', cp: 3, tp: 10, cf: null, tf: null, expected: 30 }, - { label: 'only phases data', cp: null, tp: null, cf: 2, tf: 4, expected: 50 }, - { label: 'both present uses min', cp: 8, tp: 10, cf: 3, tf: 10, expected: 30 }, - { label: 'neither returns null', cp: null, tp: null, cf: null, tf: null, expected: null }, - { label: 'total of 0 treated as no data', cp: 0, tp: 0, cf: null, tf: null, expected: null }, - ]; - - for (const { label, cp, tp, cf, tf, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.computeProgressPercent(cp, tp, cf, tf); - const cjsResult = cjs.computeProgressPercent(cp, tp, cf, tf); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -describe('state-document-generator parity: shouldPreserveExistingProgress', async () => { - const sdk = await import('../sdk/dist/state/index.js'); - - const fixtures = [ - { - label: 'existing exceeds derived on total_phases → true', - existing: { total_phases: 10 }, - derived: { total_phases: 5 }, - expected: true, - }, - { - label: 'derived exceeds existing → false', - existing: { total_phases: 5 }, - derived: { total_phases: 10 }, - expected: false, - }, - { - label: 'malformed input (non-object) → false', - existing: null, - derived: { total_phases: 5 }, - expected: false, - }, - { - label: 'both null → false', - existing: null, - derived: null, - expected: false, - }, - ]; - - for (const { label, existing, derived, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.shouldPreserveExistingProgress(existing, derived); - const cjsResult = cjs.shouldPreserveExistingProgress(existing, derived); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -describe('state-document-generator parity: normalizeProgressNumbers', async () => { - const sdk = await import('../sdk/dist/state/index.js'); - - const fixtures = [ - { - label: 'coerces all five tracked keys to numbers', - input: { total_phases: '10', completed_phases: '3', total_plans: '5', completed_plans: '2', percent: '60' }, - expected: { total_phases: 10, completed_phases: 3, total_plans: 5, completed_plans: 2, percent: 60 }, - }, - { - label: 'non-object null returned unchanged', - input: null, - expected: null, - }, - { - label: 'extra keys preserved untouched', - input: { total_phases: '4', extra_key: 'hello' }, - expected: { total_phases: 4, extra_key: 'hello' }, - }, - ]; - - for (const { label, input, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.normalizeProgressNumbers(input); - const cjsResult = cjs.normalizeProgressNumbers(input); - assert.deepStrictEqual(sdkResult, expected, `SDK: ${label}`); - assert.deepStrictEqual(cjsResult, expected, `CJS: ${label}`); - assert.deepStrictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -// SDK ESM side — dynamically import so we can test both; wrap in a top-level -// async test suite. -describe('state-document-generator parity: stateExtractField', async () => { - const sdk = await import('../sdk/dist/state/index.js'); - - const fixtures = [ - { - label: 'bold pattern', - content: 'Some content\n**FieldName:** the value\nMore content', - fieldName: 'FieldName', - expected: 'the value', - }, - { - label: 'plain pattern', - content: 'Some content\nFieldName: the value\nMore content', - fieldName: 'FieldName', - expected: 'the value', - }, - { - label: 'missing field returns null', - content: 'Some content\nOtherField: something\nMore content', - fieldName: 'FieldName', - expected: null, - }, - ]; - - for (const { label, content, fieldName, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.stateExtractField(content, fieldName); - const cjsResult = cjs.stateExtractField(content, fieldName); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); - -describe('state-document-generator parity: stateReplaceField', async () => { - const sdk = await import('../sdk/dist/state/index.js'); - - const fixtures = [ - { - label: 'bold replace', - content: 'Some content\n**Status:** old value\nMore content', - fieldName: 'Status', - newValue: 'new value', - expected: 'Some content\n**Status:** new value\nMore content', - }, - { - label: 'plain replace', - content: 'Some content\nStatus: old value\nMore content', - fieldName: 'Status', - newValue: 'new value', - expected: 'Some content\nStatus: new value\nMore content', - }, - { - label: 'missing field returns null', - content: 'Some content\nOtherField: something\nMore content', - fieldName: 'Status', - newValue: 'new value', - expected: null, - }, - ]; - - for (const { label, content, fieldName, newValue, expected } of fixtures) { - test(label, () => { - const sdkResult = sdk.stateReplaceField(content, fieldName, newValue); - const cjsResult = cjs.stateReplaceField(content, fieldName, newValue); - assert.strictEqual(sdkResult, expected, `SDK: ${label}`); - assert.strictEqual(cjsResult, expected, `CJS: ${label}`); - assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`); - }); - } -}); diff --git a/tests/workstream-inventory-builder-generator.test.cjs b/tests/workstream-inventory-builder-generator.test.cjs deleted file mode 100644 index fd4fe1f79..000000000 --- a/tests/workstream-inventory-builder-generator.test.cjs +++ /dev/null @@ -1,159 +0,0 @@ -'use strict'; - -/** - * CJS parity test — Workstream Inventory Builder generator. - * - * For every fixture, asserts that the compiled SDK ESM module and the - * generated CJS artifact produce byte-identical output. - */ - -const { describe, test, before } = require('node:test'); -const assert = require('node:assert/strict'); - -// ─── Shared fixtures ────────────────────────────────────────────────────────── - -function minimalInputs(overrides = {}) { - return { - name: 'my-ws', - projectDir: '/project', - workstreamDir: '/project/.planning/workstreams/my-ws', - phaseDirNames: [], - activeWorkstreamName: null, - phaseFilesCounts: [], - roadmapPhaseCount: 0, - stateProjection: { status: 'unknown', current_phase: null, last_activity: null }, - filesExist: { roadmap: false, state: false, requirements: false }, - ...overrides, - }; -} - -const FIXTURES = [ - { - label: 'empty inventory: no phase dirs, no STATE.md', - inputs: minimalInputs(), - }, - { - label: 'one phase in_progress (partial plan completion)', - inputs: minimalInputs({ - phaseDirNames: ['01-alpha'], - phaseFilesCounts: [{ directory: '01-alpha', planCount: 3, summaryCount: 1 }], - roadmapPhaseCount: 1, - stateProjection: { status: 'executing', current_phase: '01-alpha', last_activity: '2026-05-01' }, - filesExist: { roadmap: true, state: true, requirements: false }, - }), - }, - { - label: 'one phase complete (summary_count >= plan_count)', - inputs: minimalInputs({ - phaseDirNames: ['01-alpha'], - phaseFilesCounts: [{ directory: '01-alpha', planCount: 2, summaryCount: 2 }], - roadmapPhaseCount: 1, - stateProjection: { status: 'milestone complete', current_phase: null, last_activity: '2026-04-01' }, - filesExist: { roadmap: true, state: true, requirements: true }, - }), - }, - { - label: 'one phase pending (plan_count is 0)', - inputs: minimalInputs({ - phaseDirNames: ['01-alpha'], - phaseFilesCounts: [{ directory: '01-alpha', planCount: 0, summaryCount: 0 }], - roadmapPhaseCount: 1, - stateProjection: { status: 'planning', current_phase: null, last_activity: null }, - }), - }, - { - label: 'multiple phases with mixed statuses', - inputs: minimalInputs({ - phaseDirNames: ['01-alpha', '02-beta', '03-gamma'], - phaseFilesCounts: [ - { directory: '01-alpha', planCount: 2, summaryCount: 2 }, - { directory: '02-beta', planCount: 3, summaryCount: 1 }, - { directory: '03-gamma', planCount: 0, summaryCount: 0 }, - ], - roadmapPhaseCount: 3, - stateProjection: { status: 'executing', current_phase: '02-beta', last_activity: '2026-05-10' }, - filesExist: { roadmap: true, state: true, requirements: false }, - }), - }, - { - label: 'progress_percent clamps to 100 when completedPhases > roadmapPhaseCount', - inputs: minimalInputs({ - phaseDirNames: ['01-alpha', '02-beta', '03-gamma'], - phaseFilesCounts: [ - { directory: '01-alpha', planCount: 1, summaryCount: 1 }, - { directory: '02-beta', planCount: 1, summaryCount: 1 }, - { directory: '03-gamma', planCount: 1, summaryCount: 1 }, - ], - roadmapPhaseCount: 1, - stateProjection: { status: 'milestone complete', current_phase: null, last_activity: null }, - filesExist: { roadmap: true, state: true, requirements: false }, - }), - }, - { - label: 'active workstream marker: active: true when activeWorkstreamName === name', - inputs: minimalInputs({ - name: 'my-ws', - activeWorkstreamName: 'my-ws', - }), - }, - { - label: 'active: false when activeWorkstreamName is a different workstream', - inputs: minimalInputs({ - name: 'my-ws', - activeWorkstreamName: 'other-ws', - }), - }, -]; - -const IS_COMPLETED_FIXTURES = [ - { status: 'milestone complete', expected: true }, - { status: 'Milestone Complete', expected: true }, - { status: 'archived', expected: true }, - { status: 'Archived', expected: true }, - { status: 'executing', expected: false }, - { status: 'planning', expected: false }, - { status: 'unknown', expected: false }, - { status: '', expected: false }, -]; - -// ─── Test suite ─────────────────────────────────────────────────────────────── - -describe('workstream-inventory-builder generator parity (ESM dist vs generated CJS)', () => { - let sdkBuild, cjsModule; - - before(async () => { - // Dynamic import of the ESM SDK dist (use pathToFileURL since we're in CJS context) - const path = require('path'); - const { pathToFileURL } = require('url'); - const distPath = path.resolve(__dirname, '..', 'sdk', 'dist', 'workstream', 'builder.js'); - sdkBuild = await import(pathToFileURL(distPath).href); - // CJS require of the generated artifact - cjsModule = require('../get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs'); - }); - - describe('buildWorkstreamInventory', () => { - for (const fixture of FIXTURES) { - test(fixture.label, () => { - const sdkResult = sdkBuild.buildWorkstreamInventory(fixture.inputs); - const cjsResult = cjsModule.buildWorkstreamInventory(fixture.inputs); - assert.deepStrictEqual( - cjsResult, - sdkResult, - `Parity failure for fixture "${fixture.label}"`, - ); - }); - } - }); - - describe('isCompletedInventory', () => { - for (const { status, expected } of IS_COMPLETED_FIXTURES) { - test(`isCompletedInventory("${status}") === ${expected}`, () => { - const sdkResult = sdkBuild.isCompletedInventory(status); - const cjsResult = cjsModule.isCompletedInventory(status); - assert.strictEqual(sdkResult, expected, `SDK result mismatch for "${status}"`); - assert.strictEqual(cjsResult, expected, `CJS result mismatch for "${status}"`); - assert.strictEqual(sdkResult, cjsResult, `Parity failure for "${status}"`); - }); - } - }); -}); diff --git a/tests/workstream-name-policy-generator.test.cjs b/tests/workstream-name-policy-generator.test.cjs deleted file mode 100644 index f1301a9d0..000000000 --- a/tests/workstream-name-policy-generator.test.cjs +++ /dev/null @@ -1,145 +0,0 @@ -'use strict'; - -/** - * Parity test: workstream-name-policy.generated.cjs vs sdk/src/workstream-name-policy.ts - * - * Verifies that the generated CJS artifact matches the SDK source-of-truth - * for all exports: toWorkstreamSlug, hasInvalidPathSegment, isValidActiveWorkstreamName, - * validateWorkstreamName. - * - * Covers: Phase 6 (#3575) MIGRATE_ME resolution for workstream-name-policy.cjs. - */ - -const assert = require('assert'); -const { describe, test } = require('node:test'); -const { - toWorkstreamSlug, - hasInvalidPathSegment, - isValidActiveWorkstreamName, - validateWorkstreamName, -} = require('../get-shit-done/bin/lib/workstream-name-policy.cjs'); - -// ─── toWorkstreamSlug ──────────────────────────────────────────────────────── - -describe('workstream-name-policy — toWorkstreamSlug', () => { - test('lowercases and collapses non-alphanumeric to hyphens', () => { - assert.strictEqual(toWorkstreamSlug('My Feature Branch'), 'my-feature-branch'); - assert.strictEqual(toWorkstreamSlug('hello_world'), 'hello-world'); - assert.strictEqual(toWorkstreamSlug('API v2'), 'api-v2'); - }); - - test('strips leading/trailing hyphens', () => { - assert.strictEqual(toWorkstreamSlug('--foo--'), 'foo'); - assert.strictEqual(toWorkstreamSlug(' spaces '), 'spaces'); - }); - - test('handles empty and nullish values', () => { - assert.strictEqual(toWorkstreamSlug(''), ''); - assert.strictEqual(toWorkstreamSlug(null), ''); - assert.strictEqual(toWorkstreamSlug(undefined), ''); - }); - - test('handles already-valid slug', () => { - assert.strictEqual(toWorkstreamSlug('my-feature'), 'my-feature'); - assert.strictEqual(toWorkstreamSlug('v2'), 'v2'); - }); -}); - -// ─── hasInvalidPathSegment ─────────────────────────────────────────────────── - -describe('workstream-name-policy — hasInvalidPathSegment', () => { - test('returns true for names with forward slash', () => { - assert.strictEqual(hasInvalidPathSegment('foo/bar'), true); - }); - - test('returns true for names with backslash', () => { - assert.strictEqual(hasInvalidPathSegment('foo\\bar'), true); - }); - - test('returns true for bare dot', () => { - assert.strictEqual(hasInvalidPathSegment('.'), true); - }); - - test('returns true for double dot', () => { - assert.strictEqual(hasInvalidPathSegment('..'), true); - }); - - test('returns true for names containing dot-dot sequence', () => { - assert.strictEqual(hasInvalidPathSegment('foo..bar'), true); - assert.strictEqual(hasInvalidPathSegment('../etc'), true); - }); - - test('returns false for valid workstream names', () => { - assert.strictEqual(hasInvalidPathSegment('my-feature'), false); - assert.strictEqual(hasInvalidPathSegment('v2'), false); - assert.strictEqual(hasInvalidPathSegment('feature.experimental'), false); - assert.strictEqual(hasInvalidPathSegment('alpha_1'), false); - }); - - test('handles empty and nullish values', () => { - assert.strictEqual(hasInvalidPathSegment(''), false); - assert.strictEqual(hasInvalidPathSegment(null), false); - assert.strictEqual(hasInvalidPathSegment(undefined), false); - }); -}); - -// ─── isValidActiveWorkstreamName ───────────────────────────────────────────── - -describe('workstream-name-policy — isValidActiveWorkstreamName', () => { - test('returns true for valid alphanumeric names', () => { - assert.strictEqual(isValidActiveWorkstreamName('feature'), true); - assert.strictEqual(isValidActiveWorkstreamName('v2'), true); - assert.strictEqual(isValidActiveWorkstreamName('my-branch'), true); - assert.strictEqual(isValidActiveWorkstreamName('feature.experimental'), true); - assert.strictEqual(isValidActiveWorkstreamName('alpha_1'), true); - assert.strictEqual(isValidActiveWorkstreamName('A1'), true); - }); - - test('returns false for names starting with non-alphanumeric', () => { - assert.strictEqual(isValidActiveWorkstreamName('-feature'), false); - assert.strictEqual(isValidActiveWorkstreamName('.feature'), false); - assert.strictEqual(isValidActiveWorkstreamName('_feature'), false); - }); - - test('returns false for names with path traversal', () => { - assert.strictEqual(isValidActiveWorkstreamName('..'), false); - assert.strictEqual(isValidActiveWorkstreamName('../etc'), false); - assert.strictEqual(isValidActiveWorkstreamName('foo..bar'), false); - }); - - test('returns false for names with slashes', () => { - assert.strictEqual(isValidActiveWorkstreamName('foo/bar'), false); - assert.strictEqual(isValidActiveWorkstreamName('foo\\bar'), false); - }); - - test('returns false for names with spaces', () => { - assert.strictEqual(isValidActiveWorkstreamName('my feature'), false); - }); - - test('returns false for empty string', () => { - assert.strictEqual(isValidActiveWorkstreamName(''), false); - }); - - test('returns false for nullish values', () => { - assert.strictEqual(isValidActiveWorkstreamName(null), false); - assert.strictEqual(isValidActiveWorkstreamName(undefined), false); - }); -}); - -// ─── validateWorkstreamName (SDK alias) ────────────────────────────────────── - -describe('workstream-name-policy — validateWorkstreamName (SDK alias)', () => { - test('is an alias for isValidActiveWorkstreamName', () => { - const testCases = [ - 'feature', 'v2', 'my-branch', '-bad', '', null, undefined, - 'foo/bar', '..', 'foo..bar', 'A1', 'alpha_1', - ]; - for (const tc of testCases) { - assert.strictEqual( - validateWorkstreamName(tc), - isValidActiveWorkstreamName(tc), - `validateWorkstreamName and isValidActiveWorkstreamName should agree on: ${JSON.stringify(tc)}`, - ); - } - }); -});