fix(#2997): include phase_id_convention in resolved config (#3098)

* fix(#2997): include phase_id_convention in resolved config

_baseConfig in config-loader.cts is an explicit allowlist of keys copied
from parsed into the resolved config. phase_id_convention was in
VALID_CONFIG_KEYS (manifest line 83) and survived the unknown-key filter,
but was never copied into _baseConfig — silently dropped on a clean read.
The milestone-prefix validation check could only be activated via the
ROADMAP frontmatter fallback, not the documented project-config surface.

Added phase_id_convention: get('phase_id_convention') ?? null to _baseConfig.
3 tests: survives resolution, null round-trips, absent resolves to null.

* chore(#2997): backfill changeset PR number 3098

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-05 21:26:38 -04:00
committed by GitHub
parent 5628edddda
commit aa7697fe97
3 changed files with 47 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3098
---
**`phase_id_convention` set in `.planning/config.json` is no longer silently dropped** — the config loader's resolved-config constructor omitted the key despite it being in the valid-keys manifest, so the milestone-prefix validation check could only be activated via the ROADMAP frontmatter fallback. The key now survives resolution. (#2997)

View File

@@ -812,6 +812,7 @@ function loadConfigResolved(cwd: string, options: Record<string, unknown> = {}):
response_language: get('response_language') || null, response_language: get('response_language') || null,
claude_md_path: get('claude_md_path') || null, claude_md_path: get('claude_md_path') || null,
claude_md_assembly: (parsed['claude_md_assembly']) || null, claude_md_assembly: (parsed['claude_md_assembly']) || null,
phase_id_convention: get('phase_id_convention') ?? null,
}; };
// ADR-857 phase 3b: federated config overlay // ADR-857 phase 3b: federated config overlay

View File

@@ -1255,3 +1255,44 @@ describe("loadConfigResolved — corrupt config is distinguishable from absent",
assert.equal(res.degraded, false, "an empty file is not corruption"); assert.equal(res.degraded, false, "an empty file is not corruption");
}); });
}); });
// ─── #2997: phase_id_convention survives config resolution ─────────────────
describe('#2997: phase_id_convention is not silently dropped on a clean read', () => {
const { createTempDir } = require('./helpers.cjs');
const cfgPath = (dir) => path.join(dir, '.planning', 'config.json');
test('setting phase_id_convention in config.json survives into the resolved config', () => {
const tmpDir = createTempDir('gsd-2997-');
try {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ phase_id_convention: 'milestone-prefixed' }), 'utf-8');
const res = loadConfigResolved(tmpDir);
assert.equal(res.degraded, false, 'read must report as non-degraded');
assert.equal(res.config.phase_id_convention, 'milestone-prefixed',
`phase_id_convention must survive resolution; got: ${JSON.stringify(res.config.phase_id_convention)}`);
} finally { cleanup(tmpDir); }
});
test('phase_id_convention set to null round-trips correctly', () => {
const tmpDir = createTempDir('gsd-2997-null-');
try {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ phase_id_convention: null }), 'utf-8');
const res = loadConfigResolved(tmpDir);
assert.equal(res.config.phase_id_convention, null,
'null phase_id_convention must round-trip as null');
} finally { cleanup(tmpDir); }
});
test('phase_id_convention absent → null in resolved config (no false default)', () => {
const tmpDir = createTempDir('gsd-2997-absent-');
try {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ commit_docs: true }), 'utf-8');
const res = loadConfigResolved(tmpDir);
assert.equal(res.config.phase_id_convention, null,
'absent phase_id_convention must resolve to null, not undefined');
} finally { cleanup(tmpDir); }
});
});