* feat(#22): add plan_review.source_grounding + _authority config keys Two additive opt-out keys for the drift guard: source_grounding (bool, default true) gates the source-grounded reviewer pass; _authority (enum grep|intel|treesitter|lsp|scip, default grep) selects the resolver rung. No existing default changed. Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#22): add intel api-surface renderer + CLI subcommand Renders .planning/intel/api-map.json into a human-readable API-SURFACE.md for planner injection. Empty/missing map still writes a surface that announces itself incomplete (absence = unknown, not 'does not exist'). Gated on intel.enabled like all intel functions. Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#22): add source-grounding pass to plan-review-convergence Default-on reviewer pass (plan_review.source_grounding) that enumerates every symbol a plan cites, excludes declared new artifacts, resolves each against source via the configured authority adapter, and records three-valued verdicts. rung-0/1 MISSING is needs-acknowledgement, not a hard block; UNCHECKABLE is logged in a REVIEWS.md coverage section. Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#22): inject API-SURFACE.md into planner + require Artifacts section When intel.enabled, plan-phase regenerates API-SURFACE.md and injects it as a HINT (prefer, may be incomplete, absence = unknown), never a hard rule. Every plan must now emit an 'Artifacts this phase produces' section so the source-grounding reviewer can separate new symbols from references to existing code. Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#22): surface drift-guard in setup + settings, add docs /gsd:new-project asks to enable plan_review.source_grounding (default Y); /gsd:settings exposes the toggle and authority knob. Documents both config keys in CONFIGURATION.md, the intel api-surface command in COMMANDS.md, the drift guard in USER-GUIDE.md, and links ADR 22 from ARCHITECTURE.md. Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#22): respect AskUserQuestion 4-option cap and plan-phase XL line budget settings drift-guard toggle moved to its own 2-option question; #22 plan-phase additions condensed to bring the file back under the 1810-line XL budget without dropping the intel gate, the incomplete-surface hint, or the Artifacts-section requirement. Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#22): use live slash-command forms in drift-guard docs Doc-parity gate requires every slash-command token in docs/*.md to resolve to a registered command. Corrected the command form(s) referenced in the #22 drift-guard / api-surface documentation. The unresolved token was /gsd-core, matched from the GitHub repo reference "open-gsd/gsd-core#22" in docs/adr/22-plan-drift-guard.md. This is the same pattern as the existing 'test-runner' exemption (open-gsd/gsd-test-runner). Added 'core' to INTERNAL_COMPONENT_SLUGS with a matching explanatory comment. Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#22): add changeset fragment for drift guard (PR #487) Refs #22 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ const {
|
||||
intelSnapshot,
|
||||
intelPatchMeta,
|
||||
intelExtractExports,
|
||||
intelApiSurface,
|
||||
ensureIntelDir,
|
||||
isIntelEnabled,
|
||||
INTEL_FILES,
|
||||
@@ -703,4 +704,145 @@ describe('gsd-tools intel subcommands', () => {
|
||||
assert.strictEqual(output.valid, false);
|
||||
assert.ok(output.errors.length > 0);
|
||||
});
|
||||
|
||||
test('unknown intel subcommand error lists api-surface', () => {
|
||||
const result = runGsdTools(['intel', 'nonexistent-subcmd'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
const errorText = result.error || '';
|
||||
assert.ok(errorText.includes('api-surface'), 'error message must list api-surface');
|
||||
});
|
||||
|
||||
test('flag-looking intel subcommand treated as unknown, not crash', () => {
|
||||
const result = runGsdTools(['intel', '--api-surface'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
const errorText = result.error || '';
|
||||
assert.ok(errorText.includes('Unknown intel subcommand'), 'must emit typed unknown-subcommand error');
|
||||
});
|
||||
|
||||
test('intel api-surface returns disabled message when not enabled', () => {
|
||||
const result = runGsdTools(['intel', 'api-surface'], tmpDir);
|
||||
assert.strictEqual(result.success, true);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.disabled, true);
|
||||
});
|
||||
|
||||
test('intel api-surface writes API-SURFACE.md when enabled with populated api-map.json', () => {
|
||||
const planningDir = path.join(tmpDir, '.planning');
|
||||
enableIntel(planningDir);
|
||||
writeIntelJson(planningDir, 'api-map.json', {
|
||||
_meta: { updated_at: new Date().toISOString() },
|
||||
entries: {
|
||||
'intelQuery': { method: 'function', handler: 'intelQuery', role: 'query intel files' },
|
||||
'intelStatus': { method: 'function', handler: 'intelStatus', role: 'report freshness' },
|
||||
},
|
||||
});
|
||||
const result = runGsdTools(['intel', 'api-surface'], tmpDir);
|
||||
assert.strictEqual(result.success, true);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(output.written, 'result must include written path');
|
||||
assert.strictEqual(output.symbolCount, 2);
|
||||
const mdContent = fs.readFileSync(output.written, 'utf8');
|
||||
assert.ok(mdContent.includes('intelQuery'), 'API-SURFACE.md must list intelQuery symbol');
|
||||
assert.ok(mdContent.includes('intelStatus'), 'API-SURFACE.md must list intelStatus symbol');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── intelApiSurface ────────────────────────────────────────────────────────
|
||||
|
||||
describe('intelApiSurface', () => {
|
||||
let tmpDir;
|
||||
let planningDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
planningDir = path.join(tmpDir, '.planning');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('returns disabled response when intel is off', () => {
|
||||
const result = intelApiSurface(planningDir);
|
||||
assert.strictEqual(result.disabled, true);
|
||||
assert.ok(result.message.includes('disabled'));
|
||||
});
|
||||
|
||||
test('writes API-SURFACE.md with symbol entries from api-map.json', () => {
|
||||
enableIntel(planningDir);
|
||||
writeIntelJson(planningDir, 'api-map.json', {
|
||||
_meta: { updated_at: new Date().toISOString() },
|
||||
entries: {
|
||||
'authenticate': { method: 'POST', handler: 'authController', role: 'user login' },
|
||||
'createUser': { method: 'POST', handler: 'userController', role: 'user registration' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = intelApiSurface(planningDir);
|
||||
assert.strictEqual(result.symbolCount, 2);
|
||||
assert.ok(result.written.endsWith('API-SURFACE.md'));
|
||||
|
||||
const content = fs.readFileSync(result.written, 'utf8');
|
||||
assert.ok(content.includes('authenticate'), 'must include symbol name authenticate');
|
||||
assert.ok(content.includes('createUser'), 'must include symbol name createUser');
|
||||
assert.ok(content.includes('authController'), 'must include field value authController');
|
||||
});
|
||||
|
||||
test('writes API-SURFACE.md with incomplete banner when api-map.json is absent', () => {
|
||||
enableIntel(planningDir);
|
||||
// No api-map.json written
|
||||
|
||||
const result = intelApiSurface(planningDir);
|
||||
assert.strictEqual(result.symbolCount, 0);
|
||||
assert.ok(result.written.endsWith('API-SURFACE.md'));
|
||||
|
||||
const content = fs.readFileSync(result.written, 'utf8');
|
||||
assert.ok(content.includes('Incomplete'), 'must contain Incomplete banner when no entries');
|
||||
assert.ok(content.includes('unknown'), 'must say treat absence as "unknown"');
|
||||
});
|
||||
|
||||
test('writes API-SURFACE.md with incomplete banner when entries is empty object', () => {
|
||||
enableIntel(planningDir);
|
||||
writeIntelJson(planningDir, 'api-map.json', {
|
||||
_meta: { updated_at: new Date().toISOString() },
|
||||
entries: {},
|
||||
});
|
||||
|
||||
const result = intelApiSurface(planningDir);
|
||||
assert.strictEqual(result.symbolCount, 0);
|
||||
|
||||
const content = fs.readFileSync(result.written, 'utf8');
|
||||
assert.ok(content.includes('Incomplete'), 'empty entries must still emit incomplete banner');
|
||||
});
|
||||
|
||||
test('returns stale=false for fresh api-map.json', () => {
|
||||
enableIntel(planningDir);
|
||||
writeIntelJson(planningDir, 'api-map.json', {
|
||||
_meta: { updated_at: new Date().toISOString() },
|
||||
entries: { 'myFunc': { method: 'function' } },
|
||||
});
|
||||
|
||||
const result = intelApiSurface(planningDir);
|
||||
assert.strictEqual(result.stale, false);
|
||||
});
|
||||
|
||||
test('returns stale=true for old api-map.json', () => {
|
||||
enableIntel(planningDir);
|
||||
const oldDate = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
||||
writeIntelJson(planningDir, 'api-map.json', {
|
||||
_meta: { updated_at: oldDate },
|
||||
entries: { 'myFunc': { method: 'function' } },
|
||||
});
|
||||
|
||||
const result = intelApiSurface(planningDir);
|
||||
assert.strictEqual(result.stale, true);
|
||||
});
|
||||
|
||||
test('return shape has written, symbolCount, stale fields', () => {
|
||||
enableIntel(planningDir);
|
||||
const result = intelApiSurface(planningDir);
|
||||
assert.ok('written' in result, 'result must have written field');
|
||||
assert.ok('symbolCount' in result, 'result must have symbolCount field');
|
||||
assert.ok('stale' in result, 'result must have stale field');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user