Files
msd-core/sdk/scripts/gen-configuration.mjs
Tom Boucher 2de2d185fa feat(3536): Configuration Module via shared manifests + generator (Phase 2 of #3524)
Phase 2 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the structural drift surface that produced bug class

After this phase, neither bin/lib/ nor sdk/src/ defines
CONFIG_DEFAULTS, VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, or the
four legacy-key normalizations inline. All come from one canonical
source: the Configuration Module (sdk/src/configuration/index.ts)
+ two JSON manifests (sdk/shared/config-{defaults,schema}.manifest.json).
The CJS mirror is generator-emitted (get-shit-done/bin/lib/configuration.generated.cjs)
with a CI freshness check (sdk/scripts/check-configuration-fresh.mjs).

- sdk/shared/config-defaults.manifest.json — canonical nested defaults,
  union of CJS + SDK keys (includes security_*, post_planning_gaps,
  agent_skills, mode, every git/workflow/hooks sub-section).
- sdk/shared/config-schema.manifest.json — VALID_CONFIG_KEYS array,
  RUNTIME_STATE_KEYS array, DYNAMIC_KEY_PATTERNS array with source
  strings (regex reconstructed at runtime).
- sdk/src/configuration/index.ts — source of truth. Exports
  loadConfig (pure read), normalizeLegacyKeys (pure, idempotent,
  returns Normalization[]), mergeDefaults (deep-merge), migrateOnDisk
  (explicit opt-in disk writeback), plus CONFIG_DEFAULTS,
  VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS.
- sdk/src/configuration/index.test.ts — 29 vitest pinning tests.
- sdk/scripts/gen-configuration.mjs — generator (Function.prototype.toString()
  inspection of compiled SDK dist, plus brace-balanced text scan for
  internal helpers, matching the Phase 1 pattern).
- sdk/scripts/check-configuration-fresh.mjs — CI freshness gate.
- tests/configuration-generator.test.cjs — 27 parity assertions
  (CJS-generated == SDK source).
- tests/configuration-migrate-config.test.cjs — 3 cases for the new
  gsd-tools migrate-config subcommand.

- bin/lib/core.cjs: CONFIG_DEFAULTS literal now sources values from
  CANONICAL_CONFIG_DEFAULTS (the manifest), with a thin flat
  projection at the load boundary to preserve the existing
  flat-shape return contract for the ~21 CJS test files and 100+
  consumers. All four legacy-key migration blocks (branching_strategy,
  sub_repos, multiRepo, depth — historically lines 351-358, 388-397,
  401-408, 416-423) collapse to a single normalizeLegacyKeys call
  in each code path. The inline platformWriteSync writeback stays
  for now to preserve sync loadConfig semantics; the new async
  migrateOnDisk is reachable via gsd-tools migrate-config.
- bin/lib/config-schema.cjs: 135 → 31 lines. Re-exports from the
  generated Module.
- bin/lib/config.cjs: adds cmdMigrateConfig handler (calls
  migrateOnDisk on the explicit user-driven path).
- bin/gsd-tools.cjs: wires migrate-config into command dispatch.

- sdk/src/config.ts: re-exports CONFIG_DEFAULTS and mergeDefaults
  from the Module. loadConfig now calls normalizeLegacyKeys before
  mergeDefaults (replaces the inline branching_strategy graft).
- sdk/src/query/config-schema.ts: 160 → 36 lines. Re-exports from
  the Module.

- tests/config-schema-sdk-parity.test.cjs: refactored from
  "CJS Set equals SDK Set" (trivially true post-migration) to
  "both sides source from the manifest" — structural plus runtime
  invariant.
- Four other tests that text-grepped source files for valid keys
  (plan-review-convergence, bug-3212, bug-2492, feat-3210) are
  updated to use runtime VALID_CONFIG_KEYS.has() or manifest JSON
  lookups.

- CONTEXT.md: new Configuration Module entry with full Interface
  contract.
- Root package.json: check:configuration-fresh proxy script.
- sdk/package.json: gen:configuration + check:configuration-fresh.
- .githooks/pre-commit: configuration drift block.
- .github/workflows/test.yml: configuration drift step after the
  alias drift check.

- 9201 CJS tests pass (baseline pre-cycle: 9195; +6 net new tests
  across migrate-config + parity refactor)
- 1872 SDK vitest tests pass
- 29 Configuration Module vitest fixtures
- 27 CJS/SDK parity fixtures
- Net diff: +388 / −519 = 131-line reduction across the seven cycles,
  despite adding the new Module, manifests, generator, freshness
  check, and two new test files.

1. SDK CONFIG_DEFAULTS now includes manifest-canonical keys
   (resolve_model_ids: false, context_window: 200000, phase_naming,
   claude_md_path, git.create_tag, workflow.security_*,
   workflow.code_review_*, planning.*, hooks.workflow_guard, ship.*).
   Consumers accessing via [key: string]: unknown index get
   the manifest default instead of undefined.
2. SDK mergeDefaults is now proper recursive deep-merge instead of
   spread-per-section. Overlay { workflow: { research: false } }
   now preserves sibling workflow keys; previously it replaced
   the entire workflow section with only research + the section's
   defaults. Semantically identical for the common case;
   strictly better for partial nested overrides.
3. New gsd-tools migrate-config CLI subcommand for the explicit,
   opt-in on-disk migration path.

Closes #3536.
2026-05-15 00:02:56 -04:00

151 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Generator for get-shit-done/bin/lib/configuration.generated.cjs.
*
* Reads the compiled Configuration Module from sdk/dist/configuration/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 { readFileSync, writeFileSync } 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, '..', '..');
// ─── Read the compiled dist file for function extraction ─────────────────────
const distSrc = readFileSync(
resolve(here, '..', 'dist', 'configuration', 'index.js'),
'utf-8',
);
/**
* Extract a named function from the compiled dist source by scanning for
* `function <name>(` (or `async function <name>(`) and capturing the balanced
* braces body. Returns the full `[async] function name(...) { ... }` string,
* preserving the async keyword when present.
*/
function extractFunction(src, name) {
// Try async first, then plain function
let start = src.indexOf(`async function ${name}(`);
if (start === -1) start = src.indexOf(`function ${name}(`);
if (start === -1) throw new Error(`Function "${name}" not found in dist source`);
// Find opening brace
const braceStart = src.indexOf('{', start);
if (braceStart === -1) throw new Error(`No opening brace for "${name}"`);
// Balance braces
let depth = 0;
let i = braceStart;
while (i < src.length) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) {
return src.slice(start, i + 1);
}
}
i++;
}
throw new Error(`Unbalanced braces for "${name}"`);
}
const fnPlanningDir = extractFunction(distSrc, 'planningDir');
const fnDetectSubRepos = extractFunction(distSrc, 'detectSubRepos');
const fnDeepMergeConfig = extractFunction(distSrc, 'deepMergeConfig');
const fnNormalizeLegacyKeys = extractFunction(distSrc, 'normalizeLegacyKeys');
const fnMergeDefaults = extractFunction(distSrc, 'mergeDefaults');
const fnLoadConfig = extractFunction(distSrc, 'loadConfig');
const fnMigrateOnDisk = extractFunction(distSrc, 'migrateOnDisk');
// Capture DEPTH_TO_GRANULARITY constant
const dtgMatch = distSrc.match(/const DEPTH_TO_GRANULARITY = \{[^}]+\};/);
if (!dtgMatch) throw new Error('DEPTH_TO_GRANULARITY not found in dist source');
const dtgConst = dtgMatch[0];
// ─── Build CJS output ─────────────────────────────────────────────────────────
/**
* Build the CJS output string.
* Exported so check-configuration-fresh.mjs can call it without re-running the generator.
*/
export function buildConfigurationCjs() {
return [
`'use strict';`,
``,
`/**`,
` * GENERATED FILE — DO NOT EDIT.`,
` *`,
` * Source: sdk/src/configuration/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 ───────────────────────────────────────────────────────`,
`// Resolved relative to this file: get-shit-done/bin/lib/ → sdk/shared/`,
`// This file lives at: get-shit-done/bin/lib/configuration.generated.cjs`,
`// sdk/shared lives at: sdk/shared/ (3 dirs up from bin/lib, then into sdk/shared)`,
`const CONFIG_DEFAULTS = require('../../../sdk/shared/config-defaults.manifest.json');`,
`const SCHEMA_MANIFEST = require('../../../sdk/shared/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 => ({ ...p, test: (key) => new RegExp(p.source).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}`);
}