* chore(#2932): emit a per-invocation section manifest from init Extends the init bundle with a typed per-invocation section manifest so an invocation loads only the branch guidance it will actually take. The three flag/state-gated branches in execute-phase.md move into their own step files; the parent keeps its gsd:section markers wrapping a one-line on-demand reference, so each section's prose lives in exactly one file and the parent shrinks 93369 -> 89507 bytes. A new drift-guarded generator derives the shipped section manifest from those markers, and a new pure evaluator maps invocation facts to applicable section ids. The evaluator is a lookup over the frozen WHEN_VOCABULARY, never a parser (Greenspun's Tenth Rule, ADR-1671:69); a parity test asserts the vocabulary and the predicate map stay exhaustively in sync. Closes #2932 * fix(#2932): fail closed on prototype-chain when values An isolated adversarial review found WHEN_PREDICATES[section.when] was a bracket lookup on a plain-prototype object, so inherited Object.prototype members resolved as predicates: "constructor"/"toString"/"valueOf"/ "hasOwnProperty" returned truthy and SILENTLY INCLUDED the section, and "__proto__" threw an untyped TypeError carrying no .reason. Both violate the module's documented fail-closed contract, and the manifest is read from disk at run time so it cannot be assumed trustworthy. Builds the predicate map on a null prototype and guards the lookup with an explicit Object.hasOwn check. Adds table-driven coverage for nine Object.prototype-shaped keys asserting the TYPED reason (asserting only that it throws would still pass while broken) plus a fast-check property injecting a hostile value at an arbitrary document position. * test(#2932): retarget execute-phase step assertions at extracted step files * fix(#2932): emit typed reasons for generator lib-load and write failures * fix(#2932): restore launcher preamble in extracted steps and refresh derived fixtures * chore(#2932): backfill changeset pr number to 2987 --------- Co-authored-by: sim <sim@local>
211 lines
9.6 KiB
JavaScript
211 lines
9.6 KiB
JavaScript
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
|
|
|
function workflow(name) {
|
|
return fs.readFileSync(path.join(ROOT, 'gsd-core', 'workflows', name), 'utf8');
|
|
}
|
|
|
|
function doc(name) {
|
|
return fs.readFileSync(path.join(ROOT, 'docs', name), 'utf8');
|
|
}
|
|
|
|
function sectionBetween(content, startNeedle, endNeedle) {
|
|
const start = content.indexOf(startNeedle);
|
|
assert.ok(start !== -1, `${startNeedle} section must exist`);
|
|
const end = endNeedle ? content.indexOf(endNeedle, start) : -1;
|
|
return content.slice(start, end === -1 ? undefined : end);
|
|
}
|
|
|
|
function hookAt(point, kind, capId) {
|
|
return registry.byLoopPoint[point][kind].find((hook) => hook.capId === capId);
|
|
}
|
|
|
|
describe('ADR-857 phase 6 verification and review capability migration', () => {
|
|
test('registry declares code-review, security, and nyquist feature capabilities', () => {
|
|
for (const capId of ['code-review', 'security', 'nyquist']) {
|
|
assert.ok(registry.capabilities[capId], `${capId} capability must exist`);
|
|
assert.equal(registry.capabilities[capId].role, 'feature');
|
|
}
|
|
});
|
|
|
|
test('code-review capability owns review skills, agents, config, and execute hook', () => {
|
|
assert.equal(registry.bySkill['code-review'], 'code-review');
|
|
assert.equal(registry.byAgent['gsd-code-reviewer'], 'code-review');
|
|
assert.equal(registry.byAgent['gsd-code-fixer'], 'code-review');
|
|
assert.equal(registry.configKeys['workflow.code_review'], 'code-review');
|
|
assert.equal(registry.configKeys['workflow.code_review_depth'], 'code-review');
|
|
|
|
const hook = hookAt('execute:post', 'steps', 'code-review');
|
|
assert.ok(hook, 'code-review must register an execute:post step');
|
|
assert.deepEqual(hook.ref, { skill: 'code-review' });
|
|
assert.equal(hook.when, 'workflow.code_review');
|
|
assert.deepEqual(hook.produces, ['REVIEW.md']);
|
|
assert.ok(hook.consumes.includes('SUMMARY.md'));
|
|
});
|
|
|
|
test('security capability owns secure-phase wiring and blocking ship gate', () => {
|
|
assert.equal(registry.bySkill['secure-phase'], 'security');
|
|
assert.equal(registry.byAgent['gsd-security-auditor'], 'security');
|
|
assert.equal(registry.configKeys['workflow.security_enforcement'], 'security');
|
|
assert.equal(registry.configKeys['workflow.security_asvs_level'], 'security');
|
|
assert.equal(registry.configKeys['workflow.security_block_on'], 'security');
|
|
|
|
const step = hookAt('verify:post', 'steps', 'security');
|
|
assert.ok(step, 'security must register a verify:post step');
|
|
assert.deepEqual(step.ref, { skill: 'secure-phase' });
|
|
assert.equal(step.when, 'workflow.security_enforcement');
|
|
assert.equal(step.onError, 'halt');
|
|
|
|
const gate = hookAt('ship:pre', 'gates', 'security');
|
|
assert.ok(gate, 'security must register a blocking ship:pre gate');
|
|
assert.equal(gate.blocking, true);
|
|
assert.equal(gate.onError, 'halt');
|
|
|
|
const planContribution = hookAt('plan:pre', 'contributions', 'security');
|
|
assert.ok(planContribution, 'security must register a plan:pre contribution for threat-model guidance');
|
|
assert.equal(planContribution.into, 'planner');
|
|
assert.equal(planContribution.when, 'workflow.security_enforcement');
|
|
});
|
|
|
|
test('nyquist capability owns validate-phase wiring and config', () => {
|
|
assert.equal(registry.bySkill['validate-phase'], 'nyquist');
|
|
assert.equal(registry.byAgent['gsd-nyquist-auditor'], 'nyquist');
|
|
assert.equal(registry.configKeys['workflow.nyquist_validation'], 'nyquist');
|
|
|
|
const hook = hookAt('verify:post', 'steps', 'nyquist');
|
|
assert.ok(hook, 'nyquist must register a verify:post step');
|
|
assert.deepEqual(hook.ref, { skill: 'validate-phase' });
|
|
assert.equal(hook.when, 'workflow.nyquist_validation');
|
|
assert.deepEqual(hook.produces, ['VALIDATION.md']);
|
|
assert.ok(hook.consumes.includes('SUMMARY.md'));
|
|
});
|
|
|
|
test('autonomous code review resolves execute:post hooks instead of inlining code_review config', () => {
|
|
const content = workflow('autonomous.md');
|
|
const section = sectionBetween(
|
|
content,
|
|
'**3c.5. Code Review and Fix**',
|
|
'**3d. Post-Execution Routing**',
|
|
);
|
|
|
|
assert.ok(section.includes('loop render-hooks execute:post'));
|
|
assert.ok(section.includes('gsd-${ref.skill}'));
|
|
assert.ok(!section.includes('config-get workflow.code_review'));
|
|
});
|
|
|
|
test('execute-phase code-review gate resolves execute:post hooks instead of inlining code_review config', () => {
|
|
const content = workflow('execute-phase.md');
|
|
// close_parent_artifacts was extracted to
|
|
// gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md; the parent now
|
|
// marks that boundary with a <!-- gsd:section id="gap-closure-artifacts" --> comment
|
|
// immediately after code_review_gate's closing </step>, so it remains the correct
|
|
// end-of-step delimiter for isolating this step's body.
|
|
const section = sectionBetween(content, '<step name="code_review_gate"', '<!-- gsd:section id="gap-closure-artifacts"');
|
|
|
|
assert.ok(section.includes('loop render-hooks execute:post'));
|
|
assert.ok(section.includes('gsd-${ref.skill}'));
|
|
assert.ok(!section.includes('config-get workflow.code_review'));
|
|
});
|
|
|
|
test('quick full-mode code review resolves execute:post hooks instead of inlining code_review config', () => {
|
|
const content = workflow('quick.md');
|
|
const section = sectionBetween(content, '**Step 6.25: Code review (auto)**', '**Step 6.5: Verification');
|
|
|
|
assert.ok(section.includes('loop render-hooks execute:post'));
|
|
assert.ok(section.includes('gsd-code-reviewer'));
|
|
assert.ok(!section.includes('config-get workflow.code_review'));
|
|
});
|
|
|
|
test('code-review command self-gates through execute:post hooks', () => {
|
|
const content = workflow('code-review.md');
|
|
const section = sectionBetween(content, '<step name="check_config_gate">', '<step name="resolve_depth">');
|
|
|
|
assert.ok(section.includes('loop render-hooks execute:post'));
|
|
assert.ok(section.includes('ref.skill == "code-review"'));
|
|
assert.ok(!section.includes('config-get workflow.code_review'));
|
|
});
|
|
|
|
test('code-review-fix command self-gates through execute:post hooks', () => {
|
|
const content = workflow('code-review-fix.md');
|
|
const section = sectionBetween(content, '<step name="check_config_gate">', '<step name="check_review_exists">');
|
|
|
|
assert.ok(section.includes('loop render-hooks execute:post'));
|
|
assert.ok(section.includes('ref.skill == "code-review"'));
|
|
assert.ok(!section.includes('config-get workflow.code_review'));
|
|
});
|
|
|
|
test('verify-work resolves verify:post security hooks instead of inlining security_enforcement config', () => {
|
|
const content = workflow('verify-work.md');
|
|
const transitionStart = content.indexOf('If issues == 0:');
|
|
assert.ok(transitionStart !== -1, 'verify-work transition block must exist');
|
|
const section = content.slice(transitionStart, content.indexOf('</step>', transitionStart));
|
|
|
|
assert.ok(section.includes('loop render-hooks verify:post'));
|
|
assert.ok(section.includes('gsd-${ref.skill}'));
|
|
assert.ok(!section.includes('config-get workflow.security_enforcement'));
|
|
});
|
|
|
|
test('plan-phase threat model gate resolves plan:pre security capability hooks', () => {
|
|
const content = workflow('plan-phase.md');
|
|
const section = sectionBetween(content, '## 5.55. Security Threat Model Gate', '## 5.6. UI Design Contract Gate');
|
|
|
|
assert.ok(section.includes('loop render-hooks plan:pre'));
|
|
assert.ok(section.includes('capId == "security"'));
|
|
assert.ok(!section.includes('config-get workflow.security_enforcement'));
|
|
});
|
|
|
|
test('secure-phase command self-gates through verify:post hooks', () => {
|
|
const content = workflow('secure-phase.md');
|
|
const initFence = ['```bash', 'AUDITOR_MODEL='].join('\n');
|
|
const section = sectionBetween(content, initFence, 'Display banner:');
|
|
|
|
assert.ok(section.includes('loop render-hooks verify:post'));
|
|
assert.ok(section.includes('ref.skill == "secure-phase"'));
|
|
assert.ok(!section.includes('config-get workflow.security_enforcement'));
|
|
});
|
|
|
|
test('validate-phase command self-gates through verify:post hooks', () => {
|
|
const content = workflow('validate-phase.md');
|
|
const initFence = ['```bash', 'AUDITOR_MODEL='].join('\n');
|
|
const section = sectionBetween(content, initFence, 'Display banner:');
|
|
|
|
assert.ok(section.includes('loop render-hooks verify:post'));
|
|
assert.ok(section.includes('ref.skill == "validate-phase"'));
|
|
assert.ok(!section.includes('config-get workflow.nyquist_validation'));
|
|
});
|
|
|
|
test('documentation covers migrated review and verification capability hooks', () => {
|
|
const content = doc('reference/review-verification-capabilities.md');
|
|
const manual = doc('how-to/develop-a-capability.md');
|
|
const index = doc('README.md');
|
|
|
|
for (const term of [
|
|
'`code-review`',
|
|
'`security`',
|
|
'`nyquist`',
|
|
'`execute:post`',
|
|
'`verify:post`',
|
|
'`ship:pre`',
|
|
'gsd-tools loop render-hooks <point>',
|
|
]) {
|
|
assert.ok(content.includes(term), `capability reference must document ${term}`);
|
|
}
|
|
|
|
assert.ok(
|
|
index.includes('reference/review-verification-capabilities.md'),
|
|
'docs index must link the review and verification capabilities reference',
|
|
);
|
|
assert.ok(
|
|
manual.includes('`security` registers a `plan:pre` contribution, a `verify:post` step, and a blocking `ship:pre` gate.'),
|
|
'capability developer manual must mention review/verification hook examples',
|
|
);
|
|
});
|
|
});
|