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.
This commit is contained in:
Tom Boucher
2026-05-14 23:50:15 -04:00
parent a7f0af2ce9
commit 2de2d185fa
28 changed files with 2166 additions and 522 deletions

View File

@@ -1,155 +1,31 @@
/**
* SDK-side mirror of get-shit-done/bin/lib/config-schema.cjs.
* Thin re-export adapter — sources schema data from the Configuration Module
* (sdk/src/configuration/index.ts), which reads from the manifest at
* sdk/shared/config-schema.manifest.json.
*
* Single source of truth for valid config key paths accepted by
* `config-set`. MUST stay in sync with the CJS schema — enforced
* by tests/config-schema-sdk-parity.test.cjs (CI drift guard).
* All inline literals have been removed. The manifest is the single source
* of truth for both the SDK and CJS sides (Phase 2 Cycle 5, #3536).
*
* If you add/remove a key here, make the identical change in
* get-shit-done/bin/lib/config-schema.cjs (and vice versa). The
* parity test asserts the two allowlists are set-equal and that
* DYNAMIC_KEY_PATTERN_SOURCES produce identical regex source strings.
*
* See #2653 — CJS/SDK drift caused config-set to reject documented
* keys. #2479 added CJS↔docs parity; #2653 adds CJS↔SDK parity.
* Consumers of this module see an identical public API to the previous version:
* VALID_CONFIG_KEYS — ReadonlySet<string>
* RUNTIME_STATE_KEYS — ReadonlySet<string>
* DYNAMIC_KEY_PATTERNS — readonly DynamicKeyPattern[]
* DynamicKeyPattern — interface (re-exported type)
* isValidConfigKeyPath — (keyPath: string) => boolean
*/
/** Exact-match config key paths accepted by config-set. */
export const VALID_CONFIG_KEYS: ReadonlySet<string> = new Set([
'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
'search_gitignored', 'brave_search', 'firecrawl', 'exa_search',
'workflow.research', 'workflow.plan_check', 'workflow.verifier',
'workflow.nyquist_validation', 'workflow.ai_integration_phase', 'workflow.ui_phase', 'workflow.ui_safety_gate',
'workflow.auto_advance', 'workflow.node_repair', 'workflow.node_repair_budget',
'workflow.tdd_mode',
'workflow.human_verify_mode',
'workflow.text_mode',
'workflow.research_before_questions',
'workflow.discuss_mode',
'workflow.skip_discuss',
'workflow.auto_prune_state',
'workflow.use_worktrees',
'workflow.worktree_skip_hooks',
'workflow.code_review',
'workflow.code_review_depth',
'workflow.code_review_command',
'workflow.pattern_mapper',
'workflow.plan_bounce',
'workflow.plan_bounce_script',
'workflow.plan_bounce_passes',
'workflow.plan_chunked',
'workflow.plan_review_convergence',
'workflow.post_planning_gaps',
'workflow.security_enforcement',
'workflow.security_asvs_level',
'workflow.security_block_on',
'workflow.drift_threshold',
'workflow.drift_action',
'code_quality.fallow.enabled',
'code_quality.fallow.scope',
'code_quality.fallow.profile',
'code_quality.fallow.mcp',
'ship.pr_body_sections',
'git.branching_strategy', 'git.base_branch', 'git.create_tag', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
'planning.commit_docs', 'planning.search_gitignored', 'planning.sub_repos',
'review.default_reviewers',
'review.ollama_host', 'review.lm_studio_host', 'review.llama_cpp_host',
'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
'workflow.subagent_timeout',
'executor.stall_detect_interval_minutes',
'executor.stall_threshold_minutes',
'workflow.inline_plan_threshold',
'hooks.context_warnings',
'hooks.workflow_guard',
'workflow.context_coverage_gate',
'statusline.show_last_command',
'statusline.context_position',
'workflow.ui_review',
'workflow.max_discuss_passes',
'features.thinking_partner',
'context',
'features.global_learnings',
'learnings.max_inject',
'project_code', 'phase_naming',
'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
'response_language',
'context_window',
'intel.enabled',
'graphify.enabled',
'graphify.build_timeout',
'claude_md_path',
'claude_md_assembly.mode',
// #2517 — runtime-aware model profiles
'runtime',
// #3162 — documented top-level key: controls model ID resolution for non-Claude runtimes
'resolve_model_ids',
]);
import {
VALID_CONFIG_KEYS,
RUNTIME_STATE_KEYS,
DYNAMIC_KEY_PATTERNS,
} from '../configuration/index.js';
/**
* Internal runtime-state keys accepted by config-set workflows but not exposed
* as user-facing config options.
*/
export const RUNTIME_STATE_KEYS: ReadonlySet<string> = new Set([
'workflow._auto_chain_active',
]);
/**
* Dynamic-pattern validators — keys matching these regexes are also accepted.
* Each entry's `source` MUST equal the corresponding CJS regex `.source`
* (the parity test enforces this).
*/
export interface DynamicKeyPattern {
readonly test: (k: string) => boolean;
readonly description: string;
readonly source: string;
}
export const DYNAMIC_KEY_PATTERNS: readonly DynamicKeyPattern[] = [
{
source: '^agent_skills\\.[a-zA-Z0-9_-]+$',
description: 'agent_skills.<agent-type>',
test: (k) => /^agent_skills\.[a-zA-Z0-9_-]+$/.test(k),
},
{
source: '^review\\.models\\.[a-zA-Z0-9_-]+$',
description: 'review.models.<cli-name>',
test: (k) => /^review\.models\.[a-zA-Z0-9_-]+$/.test(k),
},
{
source: '^features\\.[a-zA-Z0-9_]+$',
description: 'features.<feature_name>',
test: (k) => /^features\.[a-zA-Z0-9_]+$/.test(k),
},
{
source: '^claude_md_assembly\\.blocks\\.[a-zA-Z0-9_]+$',
description: 'claude_md_assembly.blocks.<section>',
test: (k) => /^claude_md_assembly\.blocks\.[a-zA-Z0-9_]+$/.test(k),
},
// #2517 — runtime-aware model profile overrides: model_profile_overrides.<runtime>.<tier>
{
source: '^model_profile_overrides\\.[a-zA-Z0-9_-]+\\.(opus|sonnet|haiku)$',
description: 'model_profile_overrides.<runtime>.<opus|sonnet|haiku>',
test: (k) => /^model_profile_overrides\.[a-zA-Z0-9_-]+\.(opus|sonnet|haiku)$/.test(k),
},
// #3023 — per-phase-type model map: models.<phase_type> = <tier>
{
source: '^models\\.(planning|discuss|research|execution|verification|completion)$',
description: 'models.<planning|discuss|research|execution|verification|completion>',
test: (k) => /^models\.(planning|discuss|research|execution|verification|completion)$/.test(k),
},
// #3024 — dynamic routing with failure-tier escalation
{
source: '^dynamic_routing\\.(enabled|escalate_on_failure|max_escalations|tier_models\\.(light|standard|heavy))$',
description: 'dynamic_routing.<enabled|escalate_on_failure|max_escalations|tier_models.<light|standard|heavy>>',
test: (k) => /^dynamic_routing\.(enabled|escalate_on_failure|max_escalations|tier_models\.(light|standard|heavy))$/.test(k),
},
// #3227 — per-agent model overrides: model_overrides.<agent-id>
{
source: '^model_overrides\\.[a-zA-Z0-9_-]+$',
description: 'model_overrides.<agent-id>',
test: (k) => /^model_overrides\.[a-zA-Z0-9_-]+$/.test(k),
},
];
export {
VALID_CONFIG_KEYS,
RUNTIME_STATE_KEYS,
DYNAMIC_KEY_PATTERNS,
type DynamicKeyPattern,
} from '../configuration/index.js';
/** Returns true if keyPath is a valid config key (exact, runtime-state, or dynamic pattern). */
export function isValidConfigKeyPath(keyPath: string): boolean {