* feat: auto-remap codebase after significant phase execution (#2003) Adds a post-phase structural drift detector that compares the committed tree against `.planning/codebase/STRUCTURE.md` and either warns or auto-remaps the affected subtrees when drift exceeds a configurable threshold. ## Summary - New `bin/lib/drift.cjs` — pure detector covering four drift categories: new directories outside mapped paths, new barrel exports at `(packages|apps)/*/src/index.*`, new migration files, and new route modules. Prioritizes the most-specific category per file. - New `verify codebase-drift` CLI subcommand + SDK handler, registered as `gsd-sdk query verify.codebase-drift`. - New `codebase_drift_gate` step in `execute-phase` between `schema_drift_gate` and `verify_phase_goal`. Non-blocking by contract — any error logs and the phase continues. - Two new config keys: `workflow.drift_threshold` (int, default 3) and `workflow.drift_action` (`warn` | `auto-remap`, default `warn`), with enum/integer validation in `config-set`. - `gsd-codebase-mapper` learns an optional `--paths <p1,p2,...>` scope hint for incremental remapping; agent/workflow docs updated. - `last_mapped_commit` lives in YAML frontmatter on each `.planning/codebase/*.md` file; `readMappedCommit`/`writeMappedCommit` round-trip helpers ship in `drift.cjs`. ## Tests - 55 new tests in `tests/drift-detection.test.cjs` covering: classification, threshold gating at 2/3/4 elements, warn vs. auto-remap routing, affected-path scoping, `--paths` sanitization (traversal, absolute, shell metacharacter rejection), frontmatter round-trip, defensive paths (missing STRUCTURE.md, malformed input, non-git repos), CLI JSON output, and documentation parity. - Full suite: 5044 pass / 0 fail. ## Documentation - `docs/CONFIGURATION.md` — rows for both new keys. - `docs/ARCHITECTURE.md` — section on the post-execute drift gate. - `docs/AGENTS.md` — `--paths` flag on `gsd-codebase-mapper`. - `docs/USER-GUIDE.md` — user-facing behavior note + toggle commands. - `docs/FEATURES.md` — new 27a section with REQ-DRIFT-01..06. - `docs/INVENTORY.md` + `docs/INVENTORY-MANIFEST.json` — drift.cjs listed. - `get-shit-done/workflows/execute-phase.md` — `codebase_drift_gate` step. - `get-shit-done/workflows/map-codebase.md` — `parse_paths_flag` step. - `agents/gsd-codebase-mapper.md` — `--paths` directive under parse_focus. ## Design decisions - **Frontmatter over sidecar JSON** for `last_mapped_commit`: keeps the baseline attached to the file, survives git moves, survives per-doc regeneration, no extra file lifecycle. - **Substring match against STRUCTURE.md** for `isPathMapped`: the map is free-form markdown, not a structured manifest; any mention of a path prefix counts as "mapped territory". Cheap, no parser, zero false negatives on reasonable maps. - **Category priority migration > route > barrel > new_dir** so a file matching multiple rules counts exactly once at the most specific level. - **Empty-tree SHA fallback** (`4b825dc6…`) when `last_mapped_commit` is absent — semantically correct (no baseline means everything is drift) and deterministic across repos. - **Four layers of non-blocking** — detector try/catch, CLI try/catch, SDK handler try/catch, and workflow `|| echo` shell fallback. Any single layer failing still returns a valid skipped result. - **SDK handler delegates to `gsd-tools.cjs`** rather than re-porting the detector to TypeScript, keeping drift logic in one canonical place. Closes #2003 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(mapper): tag --paths fenced block as text (CodeRabbit MD040) Comment 3127255172. * docs(config): use /gsd- dash command syntax in drift_action row (CodeRabbit) Comment 3127255180. Matches the convention used by every other command reference in docs/CONFIGURATION.md. * fix(execute-phase): initialize AGENT_SKILLS_MAPPER + tag fenced blocks Two CodeRabbit findings on the auto-remap branch of the drift gate: - 3127255186 (must-fix): the mapper Task prompt referenced ${AGENT_SKILLS_MAPPER} but only AGENT_SKILLS (for gsd-executor) is loaded at init_context (line 72). Without this fix the literal placeholder string would leak into the spawned mapper's prompt. Add an explicit gsd-sdk query agent-skills gsd-codebase-mapper step right before the Task spawn. - 3127255183: tag the warn-message and Task() fenced code blocks as text to satisfy markdownlint MD040. * docs(map-codebase): wire PATH_SCOPE_HINT through every mapper prompt CodeRabbit (review id 4158286952, comment 3127255190) flagged that the parse_paths_flag step defined incremental-remap semantics but did not inject a normalized variable into the spawn_agents and sequential_mapping mapper prompts, so incremental remap could silently regress to a whole-repo scan. - Define SCOPED_PATHS / PATH_SCOPE_HINT in parse_paths_flag. - Inject ${PATH_SCOPE_HINT} into all four spawn_agents Task prompts. - Document the same scope contract for sequential_mapping mode. * fix(drift): writeMappedCommit tolerates missing target file CodeRabbit (review id 4158286952, drift.cjs:349-355 nitpick) noted that readMappedCommit returns null on ENOENT but writeMappedCommit threw — an asymmetry that breaks first-time stamping of a freshly produced doc that the caller has not yet written. - Catch ENOENT on the read; treat absent file as empty content. - Add a regression test that calls writeMappedCommit on a non-existent path and asserts the file is created with correct frontmatter. Test was authored to fail before the fix (ENOENT) and passes after. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.3 KiB
JavaScript
87 lines
3.3 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Single source of truth for valid config key paths.
|
|
*
|
|
* Imported by:
|
|
* - config.cjs (isValidConfigKey validator)
|
|
* - tests/config-schema-docs-parity.test.cjs (CI drift guard)
|
|
*
|
|
* Adding a key here without documenting it in docs/CONFIGURATION.md will
|
|
* fail the parity test. Adding a key to docs/CONFIGURATION.md without
|
|
* adding it here will cause config-set to reject it at runtime.
|
|
*/
|
|
|
|
/** Exact-match config key paths accepted by config-set. */
|
|
const VALID_CONFIG_KEYS = 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.text_mode',
|
|
'workflow.research_before_questions',
|
|
'workflow.discuss_mode',
|
|
'workflow.skip_discuss',
|
|
'workflow.auto_prune_state',
|
|
'workflow.use_worktrees',
|
|
'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.security_enforcement',
|
|
'workflow.security_asvs_level',
|
|
'workflow.security_block_on',
|
|
'workflow.drift_threshold',
|
|
'workflow.drift_action',
|
|
'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
|
|
'planning.commit_docs', 'planning.search_gitignored', 'planning.sub_repos',
|
|
'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
|
|
'workflow.subagent_timeout',
|
|
'workflow.inline_plan_threshold',
|
|
'hooks.context_warnings',
|
|
'hooks.workflow_guard',
|
|
'statusline.show_last_command',
|
|
'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',
|
|
]);
|
|
|
|
/**
|
|
* Dynamic-pattern validators — keys matching these regexes are also accepted.
|
|
* Each entry has a `test` function and a human-readable `description`.
|
|
*/
|
|
const DYNAMIC_KEY_PATTERNS = [
|
|
{ test: (k) => /^agent_skills\.[a-zA-Z0-9_-]+$/.test(k), description: 'agent_skills.<agent-type>' },
|
|
{ test: (k) => /^review\.models\.[a-zA-Z0-9_-]+$/.test(k), description: 'review.models.<cli-name>' },
|
|
{ test: (k) => /^features\.[a-zA-Z0-9_]+$/.test(k), description: 'features.<feature_name>' },
|
|
{ test: (k) => /^claude_md_assembly\.blocks\.[a-zA-Z0-9_]+$/.test(k), description: 'claude_md_assembly.blocks.<section>' },
|
|
];
|
|
|
|
/**
|
|
* Returns true if keyPath is a valid config key (exact or dynamic pattern).
|
|
*/
|
|
function isValidConfigKey(keyPath) {
|
|
if (VALID_CONFIG_KEYS.has(keyPath)) return true;
|
|
return DYNAMIC_KEY_PATTERNS.some((p) => p.test(keyPath));
|
|
}
|
|
|
|
module.exports = { VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, isValidConfigKey };
|