* 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:
@@ -992,3 +992,108 @@ describe('config-path command (#2282)', () => {
|
||||
assert.strictEqual(configContent.model_profile, 'quality', 'config-path should point to the file config-set wrote');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── plan_review.source_grounding + _authority (#22) ─────────────────────────
|
||||
|
||||
describe('plan_review.source_grounding and plan_review.source_grounding_authority (#22)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
// (a) Default of plan_review.source_grounding is true
|
||||
test('plan_review.source_grounding defaults to true when not set in config.json', () => {
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(
|
||||
config.plan_review?.source_grounding,
|
||||
true,
|
||||
'plan_review.source_grounding must default to true'
|
||||
);
|
||||
});
|
||||
|
||||
// (b) Default of plan_review.source_grounding_authority is "grep"
|
||||
test('plan_review.source_grounding_authority defaults to "grep" when not set in config.json', () => {
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(
|
||||
config.plan_review?.source_grounding_authority,
|
||||
'grep',
|
||||
'plan_review.source_grounding_authority must default to "grep"'
|
||||
);
|
||||
});
|
||||
|
||||
// (c) Both keys are recognized as valid config keys
|
||||
test('plan_review.source_grounding is a valid config key accepted by config-set', () => {
|
||||
const result = runGsdTools('config-set plan_review.source_grounding false', tmpDir);
|
||||
assert.ok(result.success, `config-set plan_review.source_grounding failed: ${result.error}`);
|
||||
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(config.plan_review.source_grounding, false);
|
||||
});
|
||||
|
||||
test('plan_review.source_grounding_authority is a valid config key accepted by config-set', () => {
|
||||
const result = runGsdTools('config-set plan_review.source_grounding_authority intel', tmpDir);
|
||||
assert.ok(result.success, `config-set plan_review.source_grounding_authority failed: ${result.error}`);
|
||||
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(config.plan_review.source_grounding_authority, 'intel');
|
||||
});
|
||||
|
||||
// Enum positive: all valid authority values
|
||||
test('plan_review.source_grounding_authority accepts all valid enum values', () => {
|
||||
const validValues = ['grep', 'intel', 'treesitter', 'lsp', 'scip'];
|
||||
for (const v of validValues) {
|
||||
const result = runGsdTools(`config-set plan_review.source_grounding_authority ${v}`, tmpDir);
|
||||
assert.ok(result.success, `config-set plan_review.source_grounding_authority ${v} failed: ${result.error}`);
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(config.plan_review.source_grounding_authority, v);
|
||||
}
|
||||
});
|
||||
|
||||
// (d) NEGATIVE MATRIX — invalid enum values are rejected
|
||||
test('plan_review.source_grounding_authority rejects invalid value "bogus"', () => {
|
||||
const result = runGsdTools('config-set plan_review.source_grounding_authority bogus', tmpDir);
|
||||
assert.strictEqual(result.success, false, 'bogus should be rejected');
|
||||
assert.ok(
|
||||
result.error.includes('Invalid plan_review.source_grounding_authority'),
|
||||
`Expected "Invalid plan_review.source_grounding_authority" in error: ${result.error}`
|
||||
);
|
||||
});
|
||||
|
||||
test('plan_review.source_grounding_authority rejects flag-looking value "--grep"', () => {
|
||||
const result = runGsdTools(['config-set', 'plan_review.source_grounding_authority', '--grep'], tmpDir);
|
||||
assert.strictEqual(result.success, false, '--grep should be rejected');
|
||||
assert.ok(
|
||||
result.error.includes('Invalid plan_review.source_grounding_authority'),
|
||||
`Expected "Invalid plan_review.source_grounding_authority" in error: ${result.error}`
|
||||
);
|
||||
});
|
||||
|
||||
test('plan_review.source_grounding_authority rejects empty string', () => {
|
||||
const result = runGsdTools(['config-set', 'plan_review.source_grounding_authority', ''], tmpDir);
|
||||
assert.strictEqual(result.success, false, 'empty string should be rejected');
|
||||
});
|
||||
|
||||
test('plan_review.source_grounding rejects non-boolean value "yes"', () => {
|
||||
const result = runGsdTools('config-set plan_review.source_grounding yes', tmpDir);
|
||||
assert.strictEqual(result.success, false, '"yes" should be rejected as non-boolean');
|
||||
assert.ok(
|
||||
result.error.includes('Invalid plan_review.source_grounding'),
|
||||
`Expected "Invalid plan_review.source_grounding" in error: ${result.error}`
|
||||
);
|
||||
});
|
||||
|
||||
test('plan_review.source_grounding rejects numeric value 1', () => {
|
||||
const result = runGsdTools('config-set plan_review.source_grounding 1', tmpDir);
|
||||
assert.strictEqual(result.success, false, 'numeric 1 should be rejected as non-boolean');
|
||||
assert.ok(
|
||||
result.error.includes('Invalid plan_review.source_grounding'),
|
||||
`Expected "Invalid plan_review.source_grounding" in error: ${result.error}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,6 +159,13 @@ const INTERNAL_COMPONENT_SLUGS = new Set([
|
||||
// The regex captures "/gsd-test-runner" from the URL path component. This is
|
||||
// an external tool repo, not a user-typable slash command in this product.
|
||||
'test-runner',
|
||||
|
||||
// gsd-core — GitHub repository name: "open-gsd/gsd-core".
|
||||
// docs/adr/22-plan-drift-guard.md references it as an issue tracker link:
|
||||
// open-gsd/gsd-core#22
|
||||
// The regex captures "/gsd-core" from the org/repo path separator. This is
|
||||
// the canonical repo name, not a user-typable slash command in this product.
|
||||
'core',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
198
tests/feat-22-surfacing-docs.test.cjs
Normal file
198
tests/feat-22-surfacing-docs.test.cjs
Normal file
@@ -0,0 +1,198 @@
|
||||
// allow-test-rule: docs-parity
|
||||
// Verifies that issue #22 drift-guard surfacing changes are present:
|
||||
// - new-project workflow mentions plan_review.source_grounding
|
||||
// - CONFIGURATION.md documents both new config keys
|
||||
// - COMMANDS.md mentions gsd-tools intel api-surface
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
const NEW_PROJECT_PATH = path.join(ROOT, 'get-shit-done', 'workflows', 'new-project.md');
|
||||
const SETTINGS_PATH = path.join(ROOT, 'get-shit-done', 'workflows', 'settings.md');
|
||||
const CONFIGURATION_PATH = path.join(ROOT, 'docs', 'CONFIGURATION.md');
|
||||
const COMMANDS_PATH = path.join(ROOT, 'docs', 'COMMANDS.md');
|
||||
const USER_GUIDE_PATH = path.join(ROOT, 'docs', 'USER-GUIDE.md');
|
||||
const ARCHITECTURE_PATH = path.join(ROOT, 'docs', 'ARCHITECTURE.md');
|
||||
|
||||
describe('feat-22-surfacing-docs', () => {
|
||||
// ── A1: new-project workflow ─────────────────────────────────────────────
|
||||
|
||||
test('new-project workflow mentions source_grounding', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('source_grounding'),
|
||||
'new-project.md must mention source_grounding'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project workflow has Drift Guard question', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('Drift Guard'),
|
||||
'new-project.md must include a "Drift Guard" question header'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project workflow wires source_grounding into config-new-project call', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('"plan_review":{"source_grounding":'),
|
||||
'new-project.md config-new-project call must include plan_review.source_grounding'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project workflow has Drift Guard default-yes option', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
// Both question blocks (auto and interactive) should have the yes option
|
||||
const count = (content.match(/Yes \(Recommended\).*catches hallucinated names/g) || []).length;
|
||||
assert.ok(
|
||||
count >= 1,
|
||||
'new-project.md must have at least one Drift Guard "Yes (Recommended)" option'
|
||||
);
|
||||
});
|
||||
|
||||
// ── A2: settings workflow ────────────────────────────────────────────────
|
||||
|
||||
test('settings workflow mentions source_grounding in read_current step', () => {
|
||||
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('plan_review.source_grounding'),
|
||||
'settings.md must mention plan_review.source_grounding in the read_current step'
|
||||
);
|
||||
});
|
||||
|
||||
test('settings workflow has Drift Guard AskUserQuestion toggle', () => {
|
||||
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('Drift Guard'),
|
||||
'settings.md must include a "Drift Guard" question header'
|
||||
);
|
||||
});
|
||||
|
||||
test('settings workflow update_config includes plan_review.source_grounding', () => {
|
||||
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('"source_grounding": true/false'),
|
||||
'settings.md update_config block must include source_grounding: true/false'
|
||||
);
|
||||
});
|
||||
|
||||
test('settings workflow mentions source_grounding_authority', () => {
|
||||
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('source_grounding_authority'),
|
||||
'settings.md must mention source_grounding_authority'
|
||||
);
|
||||
});
|
||||
|
||||
test('settings confirm table includes Plan Drift Guard row', () => {
|
||||
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('Plan Drift Guard'),
|
||||
'settings.md confirm table must include a "Plan Drift Guard" row'
|
||||
);
|
||||
});
|
||||
|
||||
// ── B1: CONFIGURATION.md ─────────────────────────────────────────────────
|
||||
|
||||
test('CONFIGURATION.md documents plan_review.source_grounding', () => {
|
||||
const content = fs.readFileSync(CONFIGURATION_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('`plan_review.source_grounding`'),
|
||||
'CONFIGURATION.md must document plan_review.source_grounding'
|
||||
);
|
||||
});
|
||||
|
||||
test('CONFIGURATION.md documents plan_review.source_grounding_authority', () => {
|
||||
const content = fs.readFileSync(CONFIGURATION_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('`plan_review.source_grounding_authority`'),
|
||||
'CONFIGURATION.md must document plan_review.source_grounding_authority'
|
||||
);
|
||||
});
|
||||
|
||||
test('CONFIGURATION.md documents grep as default authority', () => {
|
||||
const content = fs.readFileSync(CONFIGURATION_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('`grep`') && content.includes('source_grounding_authority'),
|
||||
'CONFIGURATION.md must document grep as the default source_grounding_authority'
|
||||
);
|
||||
});
|
||||
|
||||
test('CONFIGURATION.md lists all five authority enum values', () => {
|
||||
const content = fs.readFileSync(CONFIGURATION_PATH, 'utf-8');
|
||||
const authorities = ['grep', 'intel', 'treesitter', 'lsp', 'scip'];
|
||||
const missing = authorities.filter(a => !content.includes(a));
|
||||
assert.deepStrictEqual(
|
||||
missing,
|
||||
[],
|
||||
`CONFIGURATION.md must list all authority values; missing: ${missing.join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
// ── B2: COMMANDS.md ───────────────────────────────────────────────────────
|
||||
|
||||
test('COMMANDS.md mentions intel api-surface', () => {
|
||||
const content = fs.readFileSync(COMMANDS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('intel api-surface'),
|
||||
'COMMANDS.md must document the gsd-tools intel api-surface command'
|
||||
);
|
||||
});
|
||||
|
||||
test('COMMANDS.md documents api-surface gating on intel.enabled', () => {
|
||||
const content = fs.readFileSync(COMMANDS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('intel.enabled'),
|
||||
'COMMANDS.md intel api-surface section must mention the intel.enabled gate'
|
||||
);
|
||||
});
|
||||
|
||||
test('COMMANDS.md mentions API-SURFACE.md output', () => {
|
||||
const content = fs.readFileSync(COMMANDS_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('API-SURFACE.md'),
|
||||
'COMMANDS.md must mention the API-SURFACE.md output file'
|
||||
);
|
||||
});
|
||||
|
||||
// ── B3: USER-GUIDE.md ─────────────────────────────────────────────────────
|
||||
|
||||
test('USER-GUIDE.md has Plan Drift Guard subsection', () => {
|
||||
const content = fs.readFileSync(USER_GUIDE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('### Plan Drift Guard'),
|
||||
'USER-GUIDE.md must have a "### Plan Drift Guard" subsection'
|
||||
);
|
||||
});
|
||||
|
||||
test('USER-GUIDE.md mentions needs-acknowledgement behavior', () => {
|
||||
const content = fs.readFileSync(USER_GUIDE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('needs-acknowledgement'),
|
||||
'USER-GUIDE.md drift guard section must describe needs-acknowledgement behavior'
|
||||
);
|
||||
});
|
||||
|
||||
test('USER-GUIDE.md explains drift guard works without intel', () => {
|
||||
const content = fs.readFileSync(USER_GUIDE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('without intel') || content.includes('Works without intel'),
|
||||
'USER-GUIDE.md must explain that the drift guard works without intel'
|
||||
);
|
||||
});
|
||||
|
||||
// ── B4: ARCHITECTURE.md ───────────────────────────────────────────────────
|
||||
|
||||
test('ARCHITECTURE.md links to ADR 22', () => {
|
||||
const content = fs.readFileSync(ARCHITECTURE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('adr/22-plan-drift-guard.md') || content.includes('ADR 22'),
|
||||
'ARCHITECTURE.md must link to ADR 22 (adr/22-plan-drift-guard.md)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
137
tests/plan-phase-drift-guard.test.cjs
Normal file
137
tests/plan-phase-drift-guard.test.cjs
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Drift guard for gsd:plan-phase workflow (#22)
|
||||
*
|
||||
* Validates that the plan-phase workflow contains the key structural elements
|
||||
* added for issue #22 Change #1:
|
||||
*
|
||||
* (A) intel.enabled gate — when intel.enabled is true, plan-phase regenerates
|
||||
* API-SURFACE.md via `gsd-tools intel api-surface` and injects it into the
|
||||
* planner's required reading as a HINT (prefer symbols, may be incomplete,
|
||||
* absence = unknown, never exhaustive).
|
||||
*
|
||||
* (B) "Artifacts this phase produces" section — every PLAN.md must include
|
||||
* this section so the plan-review-convergence source-grounding pass can
|
||||
* exclude newly-created symbols from drift verification.
|
||||
*/
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// The workflow markdown IS the runtime instruction. Testing its text content
|
||||
// tests the deployed contract — if the intel gate or Artifacts section
|
||||
// requirement is absent, the drift-guard feature is absent from defenses too.
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const WORKFLOW_PATH = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'get-shit-done',
|
||||
'workflows',
|
||||
'plan-phase.md'
|
||||
);
|
||||
|
||||
// ─── Fixture ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
|
||||
|
||||
// ─── (A) intel.enabled gate ───────────────────────────────────────────────────
|
||||
|
||||
describe('plan-phase workflow: intel.enabled gate for API-SURFACE injection (#22)', () => {
|
||||
test('workflow reads intel.enabled config before planner spawn', () => {
|
||||
assert.ok(
|
||||
workflow.includes('intel.enabled'),
|
||||
'workflow must gate API-SURFACE generation on intel.enabled config key'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow runs gsd-tools intel api-surface to regenerate surface', () => {
|
||||
assert.ok(
|
||||
workflow.includes('intel api-surface'),
|
||||
'workflow must call `gsd_run intel api-surface` (or equivalent) to regenerate API-SURFACE.md'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow injects API-SURFACE.md into planner files_to_read when intel.enabled', () => {
|
||||
assert.ok(
|
||||
workflow.includes('API-SURFACE.md') && workflow.includes('API_SURFACE_PATH'),
|
||||
'workflow must pass API_SURFACE_PATH into the planner prompt files_to_read block'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow labels the surface as a HINT (not a hard rule)', () => {
|
||||
assert.ok(
|
||||
workflow.includes('HINT') || workflow.includes('intel_surface_hint'),
|
||||
'API-SURFACE.md must be annotated as a HINT, never a hard rule'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow documents that surface absence means unknown not nonexistent', () => {
|
||||
assert.ok(
|
||||
workflow.includes("absence means *unknown*, not *nonexistent*") ||
|
||||
workflow.includes("absence = unknown") ||
|
||||
workflow.includes("absence means unknown"),
|
||||
"workflow must state that a symbol's absence from the surface means unknown, not nonexistent"
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow states the surface may be incomplete', () => {
|
||||
assert.ok(
|
||||
workflow.includes('MAY BE INCOMPLETE') || workflow.includes('may be incomplete'),
|
||||
'workflow must warn that the API surface may be incomplete'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow skips surface injection when intel.enabled is false', () => {
|
||||
// The gate must have an explicit false/skip branch
|
||||
assert.ok(
|
||||
workflow.includes("INTEL_CFG") &&
|
||||
(workflow.includes("'false'") || workflow.includes('"false"') || workflow.includes('false')),
|
||||
'workflow must skip the intel step when intel.enabled is false (config defaults to false)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── (B) "Artifacts this phase produces" requirement ─────────────────────────
|
||||
|
||||
describe('plan-phase workflow: Artifacts this phase produces section (#22)', () => {
|
||||
test('downstream_consumer block requires Artifacts this phase produces section', () => {
|
||||
assert.ok(
|
||||
workflow.includes('Artifacts this phase produces'),
|
||||
'downstream_consumer must list "Artifacts this phase produces" as a required plan section'
|
||||
);
|
||||
});
|
||||
|
||||
test('quality_gate checklist includes Artifacts this phase produces item', () => {
|
||||
// Find the quality_gate block and confirm the checklist item is there
|
||||
const qualityGateMatch = workflow.match(/<quality_gate>([\s\S]*?)<\/quality_gate>/);
|
||||
assert.ok(
|
||||
qualityGateMatch,
|
||||
'workflow must have a <quality_gate> block'
|
||||
);
|
||||
assert.ok(
|
||||
qualityGateMatch[1].includes('Artifacts this phase produces'),
|
||||
'<quality_gate> checklist must include an "Artifacts this phase produces" item'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow explains why Artifacts section is needed (source-grounding reviewer)', () => {
|
||||
assert.ok(
|
||||
workflow.includes('source-grounding') || workflow.includes('plan-review-convergence'),
|
||||
'workflow must explain that the Artifacts section is consumed by the source-grounding pass'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow lists symbol kinds for Artifacts section (decorators, classes, functions, CLI flags)', () => {
|
||||
// Must enumerate concrete symbol kinds so planner knows what to list
|
||||
const hasDecorators = workflow.includes('decorators');
|
||||
const hasClasses = workflow.includes('classes');
|
||||
const hasFunctions = workflow.includes('functions');
|
||||
const hasCliFlags = workflow.includes('CLI flags');
|
||||
assert.ok(
|
||||
hasDecorators && hasClasses && hasFunctions && hasCliFlags,
|
||||
'workflow must enumerate symbol kinds: decorators, classes, functions, CLI flags (needed for Artifacts section guidance)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -566,6 +566,47 @@ describe('plan-review-convergence local model config schema registration (#2306-
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Workflow: source-grounding pass (#22) ───────────────────────────────────
|
||||
|
||||
describe('plan-review-convergence workflow: source-grounding reviewer pass (#22)', () => {
|
||||
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
|
||||
|
||||
test('workflow documents plan_review.source_grounding config key (default on)', () => {
|
||||
assert.ok(
|
||||
workflow.includes('plan_review.source_grounding'),
|
||||
'workflow must document the plan_review.source_grounding config key that gates the source-grounding pass (#22)'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow defines all four symbol verdicts: VERIFIED, MISSING, AMBIGUOUS, UNCHECKABLE', () => {
|
||||
assert.ok(workflow.includes('VERIFIED'), 'workflow must define VERIFIED verdict');
|
||||
assert.ok(workflow.includes('MISSING'), 'workflow must define MISSING verdict');
|
||||
assert.ok(workflow.includes('AMBIGUOUS'), 'workflow must define AMBIGUOUS verdict');
|
||||
assert.ok(workflow.includes('UNCHECKABLE'), 'workflow must define UNCHECKABLE verdict');
|
||||
});
|
||||
|
||||
test('workflow specifies needs-acknowledgement gating for MISSING symbols', () => {
|
||||
assert.ok(
|
||||
workflow.includes('needs-acknowledgement'),
|
||||
'workflow must specify needs-acknowledgement (not hard block) for MISSING at grep/intel authority (#22)'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow instructs reviewer to exclude symbols declared under "Artifacts this phase produces"', () => {
|
||||
assert.ok(
|
||||
workflow.includes('Artifacts this phase produces'),
|
||||
'workflow must exclude new artifacts declared by the plan from symbol verification (#22)'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow requires "Verification coverage" section appended to REVIEWS.md', () => {
|
||||
assert.ok(
|
||||
workflow.includes('Verification coverage'),
|
||||
'workflow must require a Verification coverage section in REVIEWS.md listing every UNCHECKABLE/skipped symbol (#22)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plan-review-convergence local model CONFIGURATION.md documentation (#2306-local)', () => {
|
||||
const configDoc = fs.readFileSync(CONFIG_DOC_PATH, 'utf8');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user