From 6425a3cb72373667c227980f8628d7ae0d7f4ace Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 20 Jun 2026 16:37:54 -0400 Subject: [PATCH 1/2] fix(#1493): read workflow.drift_action/drift_threshold from nested config shape in verify.cts loadConfig() returns a flattened object with no nested `workflow` key, so config?.workflow was always undefined, making drift_action permanently 'warn' and drift_threshold permanently 3 regardless of .planning/config.json. Fixes by reading the raw config.json directly (matching the pattern in check-command-router.cts:readWorkflowConfig). Adds two behavioral regression tests that fail under the old code and pass under the fix. Closes #1493 Co-Authored-By: Claude Sonnet 4.6 --- src/verify.cts | 14 ++++++- tests/drift-detection.test.cjs | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/verify.cts b/src/verify.cts index fa222088e..c5e2c4bb3 100644 --- a/src/verify.cts +++ b/src/verify.cts @@ -2210,8 +2210,18 @@ function cmdVerifyCodebaseDrift(cwd: string, raw: boolean): void { else if (status === 'D') deleted.push(file); } - const config = loadConfig(cwd); - const wf = config?.workflow as Record | undefined; + // loadConfig() returns a flattened object — there is no nested `workflow` + // key. Read the raw config.json directly to access workflow-scoped keys, + // matching the pattern used in check-command-router.cts:readWorkflowConfig. + let wf: Record | undefined; + try { + const rawCfg = JSON.parse( + fs.readFileSync(path.join(planningDir(cwd), 'config.json'), 'utf-8'), + ) as Record; + wf = rawCfg['workflow'] as Record | undefined; + } catch { + wf = undefined; + } const threshold = Number.isInteger(wf?.drift_threshold) && (wf?.drift_threshold as number) >= 1 ? (wf?.drift_threshold as number) diff --git a/tests/drift-detection.test.cjs b/tests/drift-detection.test.cjs index ebdfc52b4..ebb59615d 100644 --- a/tests/drift-detection.test.cjs +++ b/tests/drift-detection.test.cjs @@ -720,3 +720,77 @@ describe('verify codebase-drift CLI', () => { } }); }); + +// ─── Regression #1493 — workflow.drift_action / drift_threshold read from nested config shape ─── +// +// loadConfig() returns a flattened object; config?.workflow was always undefined, +// making drift_action permanently 'warn' and drift_threshold always 3 regardless +// of .planning/config.json contents. Fix reads the raw nested JSON directly. + +describe('verify codebase-drift — workflow config read from nested shape (#1493)', () => { + let tmp; + beforeEach(() => { + tmp = createTempGitProject('gsd-drift-1493-'); + fs.mkdirSync(path.join(tmp, '.planning', 'codebase'), { recursive: true }); + }); + afterEach(() => cleanup(tmp)); + + test('workflow.drift_action=auto-remap in config.json is honored (not always warn) (#1493)', () => { + // Write config with nested workflow shape — the flat loadConfig() path would + // have silently dropped this, leaving action === 'warn'. + fs.writeFileSync( + path.join(tmp, '.planning', 'config.json'), + JSON.stringify({ workflow: { drift_action: 'auto-remap', drift_threshold: 1 } }, null, 2), + ); + + // Map codebase to current HEAD so anything committed next is "new" drift. + const structure = path.join(tmp, '.planning', 'codebase', 'STRUCTURE.md'); + fs.writeFileSync(structure, '# Codebase Structure\n\n- `src/`\n'); + writeMappedCommit(structure, git(tmp, 'rev-parse', 'HEAD'), '2026-04-22'); + git(tmp, 'add', '-A'); + git(tmp, 'commit', '-m', 'map codebase'); + + // Add one structural barrel file — enough to exceed drift_threshold of 1. + const dir = path.join(tmp, 'packages', 'ui', 'src'); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'index.ts'), 'export {};\n'); + git(tmp, 'add', '-A'); + git(tmp, 'commit', '-m', 'add package barrel'); + + const r = runGsdTools(['verify', 'codebase-drift'], tmp); + assert.strictEqual(r.success, true, r.error); + const data = JSON.parse(r.output); + assert.strictEqual( + data.action, 'auto-remap', + 'workflow.drift_action=auto-remap must flow through from nested config; "warn" means the flat-shape bug is still active', + ); + }); + + test('workflow.drift_threshold in config.json gates triggering (#1493)', () => { + // Threshold of 100 — 1 structural file should not trigger action_required. + fs.writeFileSync( + path.join(tmp, '.planning', 'config.json'), + JSON.stringify({ workflow: { drift_action: 'auto-remap', drift_threshold: 100 } }, null, 2), + ); + + const structure = path.join(tmp, '.planning', 'codebase', 'STRUCTURE.md'); + fs.writeFileSync(structure, '# Codebase Structure\n\n- `src/`\n'); + writeMappedCommit(structure, git(tmp, 'rev-parse', 'HEAD'), '2026-04-22'); + git(tmp, 'add', '-A'); + git(tmp, 'commit', '-m', 'map codebase'); + + const dir = path.join(tmp, 'packages', 'ui', 'src'); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'index.ts'), 'export {};\n'); + git(tmp, 'add', '-A'); + git(tmp, 'commit', '-m', 'add one package barrel'); + + const r = runGsdTools(['verify', 'codebase-drift'], tmp); + assert.strictEqual(r.success, true, r.error); + const data = JSON.parse(r.output); + assert.strictEqual(data.threshold, 100, + 'workflow.drift_threshold=100 must be read from nested config; 3 means the flat-shape bug is still active'); + assert.strictEqual(data.action_required, false, + '1 structural file must not exceed threshold of 100'); + }); +}); From 6fe5aa5c8d441dce079043c5c923fe9350cec11b Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 20 Jun 2026 16:39:32 -0400 Subject: [PATCH 2/2] chore(#1493): add changeset for verify codebase-drift nested config fix (#1504) Co-Authored-By: Claude Sonnet 4.6 --- .changeset/lively-orcas-roam.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lively-orcas-roam.md diff --git a/.changeset/lively-orcas-roam.md b/.changeset/lively-orcas-roam.md new file mode 100644 index 000000000..0cd9159a7 --- /dev/null +++ b/.changeset/lively-orcas-roam.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1504 +--- +`verify codebase-drift` now reads `workflow.drift_action` and `workflow.drift_threshold` from the correct nested config shape — previously both keys silently no-oped because `loadConfig()` returns a flattened object and `config?.workflow` was always `undefined`.