@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/__tests__/codex-hooks.test.js +444 -14
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +615 -185
- package/bin/__tests__/installer-safety.test.js +39 -0
- package/bin/__tests__/opencode-spec-gate.test.js +24 -16
- package/bin/__tests__/package-inventory.test.js +687 -13
- package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +1914 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +143 -99
- package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
- package/src/claude/hooks/completion-authority-check.cjs +37 -288
- package/src/claude/hooks/completion-authority-state.cjs +10 -8
- package/src/claude/hooks/completion-authority.cjs +13 -7
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +77 -30
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/spec-gate.cjs +47 -117
- package/src/claude/hooks/spec-state.cjs +8 -2
- package/src/claude/migration-manifest.json +9 -1
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/spec-final-state.cjs +244 -0
- package/src/claude/scripts/spec-ground.cjs +535 -173
- package/src/claude/scripts/spec-readiness.cjs +118 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +67 -16
- package/src/claude/scripts/spec-scaffold.cjs +1354 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +2782 -142
- package/src/claude/scripts/workflow-policy.cjs +501 -183
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/code-review/SKILL.md +12 -3
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +47 -60
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +19 -12
- package/src/claude/skills/specs/SKILL.md +183 -177
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +200 -260
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +23 -85
- package/src/claude/skills/specs/templates/task.md +30 -102
- package/src/claude/skills/sync/SKILL.md +33 -10
- package/src/claude/skills/sync/references/sync-protocols.md +31 -13
- package/src/claude/skills/test/SKILL.md +17 -7
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +43 -240
- package/src/codex/hooks/completion-authority-state.cjs +10 -8
- package/src/codex/hooks/completion-authority.cjs +13 -7
- package/src/codex/hooks/lib/hook-context.cjs +16 -2
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +38 -16
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
- package/src/codex/hooks/lib/spec-utils.cjs +56 -5
- package/src/codex/hooks/privacy-approval.cjs +1 -1
- package/src/codex/hooks/privacy-block.cjs +67 -11
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/spec-gate.cjs +49 -34
- package/src/codex/hooks/spec-state.cjs +10 -3
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +6 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
|
@@ -12,7 +12,15 @@ const PACKAGE_ROOT = path.join(__dirname, '../..');
|
|
|
12
12
|
const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
|
|
13
13
|
const POLICY = require(POLICY_PATH);
|
|
14
14
|
const PROVENANCE_HELPER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/provenance.cjs'));
|
|
15
|
+
const FINAL_STATE = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-final-state.cjs'));
|
|
16
|
+
const VALIDATOR = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-spec-output.cjs'));
|
|
17
|
+
const GROUNDER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-ground.cjs'));
|
|
18
|
+
const SEMANTIC_AUTHORITY = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs'));
|
|
19
|
+
const READINESS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'));
|
|
20
|
+
const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
|
|
15
21
|
const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
|
|
22
|
+
const SPECS = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md');
|
|
23
|
+
const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
|
|
16
24
|
const GATE = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/quality-gate.md');
|
|
17
25
|
const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
|
|
18
26
|
const SYNC_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/sync/SKILL.md');
|
|
@@ -49,6 +57,183 @@ function initFixtureGit(root) {
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs') {
|
|
61
|
+
const destinationRoot = path.join(fixtureRoot, '.claude');
|
|
62
|
+
copyClaudeTestRuntime(PACKAGE_ROOT, destinationRoot);
|
|
63
|
+
const hooksRoot = path.join(PACKAGE_ROOT, 'src', 'claude', 'hooks');
|
|
64
|
+
for (const hook of [
|
|
65
|
+
'spec-gate.cjs',
|
|
66
|
+
'completion-authority-check.cjs',
|
|
67
|
+
'completion-authority-state.cjs',
|
|
68
|
+
'semantic-review-authority.cjs',
|
|
69
|
+
]) {
|
|
70
|
+
const target = path.join(destinationRoot, 'hooks', hook);
|
|
71
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
72
|
+
fs.copyFileSync(path.join(hooksRoot, hook), target);
|
|
73
|
+
}
|
|
74
|
+
return path.join(destinationRoot, entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function markdownSection(content, heading) {
|
|
78
|
+
const marker = `## ${heading}`;
|
|
79
|
+
const start = content.indexOf(marker);
|
|
80
|
+
assert.notEqual(start, -1, `missing section ${heading}`);
|
|
81
|
+
const rest = content.slice(start + marker.length);
|
|
82
|
+
const end = rest.search(/\n## /);
|
|
83
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function markdownTable(section, firstHeader) {
|
|
87
|
+
const lines = section.split('\n').filter((line) => line.trim().startsWith('|'));
|
|
88
|
+
const cells = (line) => line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
89
|
+
const headerIndex = lines.findIndex((line) => cells(line)[0] === firstHeader);
|
|
90
|
+
assert.notEqual(headerIndex, -1, `missing table headed by ${firstHeader}`);
|
|
91
|
+
const headers = cells(lines[headerIndex]);
|
|
92
|
+
const rows = {};
|
|
93
|
+
for (const line of lines.slice(headerIndex + 2)) {
|
|
94
|
+
const values = cells(line);
|
|
95
|
+
if (values.length !== headers.length) break;
|
|
96
|
+
rows[values[0].replaceAll('`', '')] = Object.fromEntries(headers.map((header, index) => [header, values[index]]));
|
|
97
|
+
}
|
|
98
|
+
return rows;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runNode(root, script, args) {
|
|
102
|
+
return spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function commandOutput(result) {
|
|
106
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createCanonicalAuthoringFixture() {
|
|
110
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-develop-contract-'));
|
|
111
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
112
|
+
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
113
|
+
fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
|
|
114
|
+
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { enabled: true };\n');
|
|
115
|
+
fs.writeFileSync(path.join(root, 'test', 'service.test.js'), 'require("node:test")("service", () => {});\n');
|
|
116
|
+
const scaffold = runNode(root, SCAFFOLD, ['feature']);
|
|
117
|
+
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
118
|
+
const specDir = path.join(root, 'specs', 'feature');
|
|
119
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
120
|
+
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
### Requirement 1: Observable service behavior
|
|
124
|
+
- **R1.1**: When valid input arrives, the service shall return enabled state; invalid input shall return rejected state.
|
|
125
|
+
- **R1.2**: The verifier shall produce separate observable proof for enabled and rejected states.
|
|
126
|
+
`);
|
|
127
|
+
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
128
|
+
|
|
129
|
+
## Architecture
|
|
130
|
+
The entrypoint exposes the bounded service behavior and a separate proof artifact.
|
|
131
|
+
|
|
132
|
+
## Typed Anchors
|
|
133
|
+
| ID | Type | Target | Role | Access | Action |
|
|
134
|
+
|---|---|---|---|---|---|
|
|
135
|
+
| A-D-01 | file | \`src/entry.js\` | service boundary | read | read |
|
|
136
|
+
| A-D-02 | artifact | \`artifacts/design-proof.json\` | proof evidence | write | create |
|
|
137
|
+
|
|
138
|
+
## Canonical Contracts & Invariants
|
|
139
|
+
### D1 — Dispatch decision
|
|
140
|
+
The entrypoint dispatches exactly once.
|
|
141
|
+
### I1 — Invalid-state invariant
|
|
142
|
+
Invalid input never returns enabled state.
|
|
143
|
+
### C1 — Result contract
|
|
144
|
+
The result contains one enabled boolean.
|
|
145
|
+
|
|
146
|
+
## Verification Definitions
|
|
147
|
+
- **V1**: Subject criteria R1.1; Subject owner A-D-01; Proof criteria R1.2; Proof owner A-D-02; Evidence anchor A-D-02; Decision refs D1, I1, C1; Method command \`node --test test/service.test.js\`; Expected exit 0 with enabled state and separate proof evidence; Negative/failure invalid input returns rejected state without enabled output; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01, A-D-02.
|
|
148
|
+
`);
|
|
149
|
+
return { root, specDir, specPath };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createOrdinaryAuthoringFixture() {
|
|
153
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-ordinary-readiness-'));
|
|
154
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
155
|
+
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
156
|
+
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
157
|
+
fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), 'require("node:test")("entry", () => {});\n');
|
|
158
|
+
const scaffold = runNode(root, SCAFFOLD, ['ordinary']);
|
|
159
|
+
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
160
|
+
const specDir = path.join(root, 'specs', 'ordinary');
|
|
161
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
162
|
+
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
163
|
+
|
|
164
|
+
## Requirements
|
|
165
|
+
### Requirement 1: Delete behavior
|
|
166
|
+
- **R1.1**: When deletion is requested, the service shall delete the selected record and report deleted state.
|
|
167
|
+
`);
|
|
168
|
+
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
169
|
+
|
|
170
|
+
## Typed Anchors
|
|
171
|
+
| ID | Type | Target | Role | Access | Action |
|
|
172
|
+
|---|---|---|---|---|---|
|
|
173
|
+
| A-D-01 | file | \`src/entry.js\` | behavior owner | read | read |
|
|
174
|
+
|
|
175
|
+
## Canonical Contracts & Invariants
|
|
176
|
+
### D1 — Delete decision
|
|
177
|
+
The selected record is deleted rather than archived.
|
|
178
|
+
### I1 — State invariant
|
|
179
|
+
Deleted state is reported only after deletion succeeds.
|
|
180
|
+
### C1 — Result contract
|
|
181
|
+
The result contains the literal deleted state.
|
|
182
|
+
|
|
183
|
+
## Verification Definitions
|
|
184
|
+
- **V1**: Criteria R1.1; Owner A-D-01; Decision refs D1, I1, C1; Method command \`node --test test/entry.test.js\`; Expected exit 0 and the service reports deleted state; Negative/failure a missing record returns not-found without deleted state; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01.
|
|
185
|
+
`);
|
|
186
|
+
const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
187
|
+
spec.authoring.requirements = 'validated';
|
|
188
|
+
spec.authoring.design = 'validated';
|
|
189
|
+
fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
190
|
+
initFixtureGit(root);
|
|
191
|
+
return { root, specDir, specPath };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ordinaryReview() {
|
|
195
|
+
return {
|
|
196
|
+
reviewed_criteria: ['R1.1'],
|
|
197
|
+
counterexamples: [{
|
|
198
|
+
criterion: 'R1.1', case_kind: 'failure',
|
|
199
|
+
scenario: 'Deletion targets a record that does not exist in the current store.',
|
|
200
|
+
expected: 'The service returns not-found and never reports the record as deleted.',
|
|
201
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
202
|
+
}],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function promoteCanonicalModel(fixture) {
|
|
207
|
+
const result = runNode(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
|
|
208
|
+
assert.equal(result.status, 0, commandOutput(result));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function makeCanonicalReady(fixture) {
|
|
212
|
+
const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
213
|
+
spec.authoring.requirements = 'validated';
|
|
214
|
+
spec.authoring.design = 'validated';
|
|
215
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
216
|
+
const result = READINESS.finalizeReadiness({
|
|
217
|
+
specDir: fixture.specDir,
|
|
218
|
+
projectRoot: fixture.root,
|
|
219
|
+
reviewResult: {
|
|
220
|
+
reviewed_criteria: ['R1.1', 'R1.2'],
|
|
221
|
+
counterexamples: [{
|
|
222
|
+
criterion: 'R1.1', case_kind: 'failure',
|
|
223
|
+
scenario: 'The service receives invalid input without its required discriminator.',
|
|
224
|
+
expected: 'The service returns rejected state and never emits enabled true.',
|
|
225
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
226
|
+
}, {
|
|
227
|
+
criterion: 'R1.2', case_kind: 'adversarial',
|
|
228
|
+
scenario: 'The service reports enabled state without producing separate proof evidence.',
|
|
229
|
+
expected: 'Verification fails because the independent proof artifact is absent.',
|
|
230
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
231
|
+
}],
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
return result.spec;
|
|
235
|
+
}
|
|
236
|
+
|
|
52
237
|
function bindFixtureReceipt(root, value, feature = 'demo', session = 'test') {
|
|
53
238
|
const context = PROVENANCE_HELPER.deriveRuntimeContext({
|
|
54
239
|
projectRoot: root,
|
|
@@ -121,7 +306,7 @@ test('delegation plan consumes legacy tiers as obligations without an agent chai
|
|
|
121
306
|
const standard = POLICY.delegationPlan({ tier: 'Standard', mode: 'specific-task' });
|
|
122
307
|
const deep = POLICY.delegationPlan({ tier: 'Deep', mode: 'full-spec', taskCount: 2 });
|
|
123
308
|
assert.deepEqual(light.proof_obligations, ['needsExecutionProof']);
|
|
124
|
-
assert.deepEqual(standard.proof_obligations, ['
|
|
309
|
+
assert.deepEqual(standard.proof_obligations, ['needsExecutionProof']);
|
|
125
310
|
assert.deepEqual(deep.proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
126
311
|
for (const plan of [light, standard, deep]) {
|
|
127
312
|
assert.equal(Object.hasOwn(plan, 'delegated'), false);
|
|
@@ -144,31 +329,53 @@ test('lane classifier selects Direct for explicit reversible low-risk work', ()
|
|
|
144
329
|
assert.equal(policy.proof_obligations.includes('needsDurableTaskState'), false);
|
|
145
330
|
});
|
|
146
331
|
|
|
147
|
-
test('lane classifier
|
|
332
|
+
test('lane classifier raises named risks to Elevated without inventing independent-audit authority', () => {
|
|
148
333
|
const result = POLICY.classifyLane({
|
|
149
334
|
reversible: true,
|
|
150
335
|
riskSignals: { auth: true, migration: true, publicContract: true },
|
|
151
336
|
});
|
|
152
|
-
assert.equal(result.lane, '
|
|
337
|
+
assert.equal(result.lane, 'Standard');
|
|
338
|
+
assert.equal(result.assuranceLevel, 'Elevated');
|
|
339
|
+
assert.equal(result.automaticAssuranceLevel, 'Elevated');
|
|
153
340
|
assert.deepEqual(result.risks, ['auth', 'migration', 'publicContract']);
|
|
154
|
-
assert.deepEqual(POLICY.
|
|
155
|
-
'needsInspection', 'needsExecutionProof',
|
|
341
|
+
assert.deepEqual(POLICY.lanePolicy(result).proof_obligations, [
|
|
342
|
+
'needsInspection', 'needsExecutionProof',
|
|
156
343
|
]);
|
|
344
|
+
|
|
345
|
+
const strict = POLICY.classifyLane({
|
|
346
|
+
riskSignals: { auth: true },
|
|
347
|
+
assurance_level: 'Strict',
|
|
348
|
+
});
|
|
349
|
+
assert.equal(strict.lane, 'Critical');
|
|
350
|
+
assert.equal(strict.automaticLane, 'Standard');
|
|
351
|
+
assert.equal(strict.assuranceLevel, 'Strict');
|
|
352
|
+
assert.equal(strict.automaticAssuranceLevel, 'Elevated');
|
|
353
|
+
assert.ok(POLICY.lanePolicy(strict).proof_obligations.includes('needsIndependentAudit'));
|
|
157
354
|
});
|
|
158
355
|
|
|
159
|
-
test('P1 persists
|
|
356
|
+
test('P1 persists the minimal v2.1 policy and derives compatibility views', () => {
|
|
160
357
|
const initial = POLICY.persistWorkflowPolicySnapshot(
|
|
161
|
-
{ feature_name: 'bounded'
|
|
358
|
+
{ feature_name: 'bounded' },
|
|
162
359
|
{ riskSignals: {} },
|
|
163
360
|
);
|
|
164
361
|
const snapshot = initial.workflow_policy;
|
|
165
|
-
assert.deepEqual(Object.keys(snapshot).sort(), [...POLICY.
|
|
166
|
-
assert.equal(snapshot.version,
|
|
167
|
-
assert.equal(snapshot.
|
|
168
|
-
assert.equal(snapshot.
|
|
169
|
-
assert.
|
|
170
|
-
|
|
171
|
-
|
|
362
|
+
assert.deepEqual(Object.keys(snapshot).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
363
|
+
assert.equal(snapshot.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
364
|
+
assert.equal(snapshot.planning_depth, 'Compact');
|
|
365
|
+
assert.equal(snapshot.assurance_level, 'Routine');
|
|
366
|
+
assert.deepEqual(snapshot.classified_minimum, {
|
|
367
|
+
planning_depth: 'Compact',
|
|
368
|
+
assurance_level: 'Routine',
|
|
369
|
+
});
|
|
370
|
+
for (const derivedField of ['lane', 'automatic_lane', 'artifact_profile', 'proof_obligations', 'actor_needs']) {
|
|
371
|
+
assert.equal(Object.hasOwn(snapshot, derivedField), false, `${derivedField} must not be persisted`);
|
|
372
|
+
}
|
|
373
|
+
const view = POLICY.readWorkflowPolicySnapshot(initial);
|
|
374
|
+
assert.equal(view.lane, 'Standard');
|
|
375
|
+
assert.equal(view.automatic_lane, 'Standard');
|
|
376
|
+
assert.equal(view.artifact_profile, 'bounded');
|
|
377
|
+
assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
|
|
378
|
+
assert.equal(Object.hasOwn(initial, 'override_' + 'receipt'), false);
|
|
172
379
|
assert.equal(POLICY.validateWorkflowPolicySnapshot(snapshot).valid, true);
|
|
173
380
|
|
|
174
381
|
const persistedAgain = POLICY.persistWorkflowPolicySnapshot(initial, {
|
|
@@ -177,16 +384,16 @@ test('P1 persists one strict workflow-policy snapshot and rejects malformed snap
|
|
|
177
384
|
});
|
|
178
385
|
assert.deepEqual(persistedAgain.workflow_policy, snapshot, 'persist-once must not reclassify an existing snapshot');
|
|
179
386
|
|
|
180
|
-
const malformed = { ...snapshot,
|
|
387
|
+
const malformed = { ...snapshot, lane: 'Critical' };
|
|
181
388
|
const validation = POLICY.validateWorkflowPolicySnapshot(malformed);
|
|
182
389
|
assert.equal(validation.valid, false);
|
|
183
|
-
assert.match(validation.errors.join('; '), /fields must be exactly
|
|
390
|
+
assert.match(validation.errors.join('; '), /v2\.1 fields must be exactly/);
|
|
184
391
|
assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: malformed }), /Invalid workflow policy snapshot/);
|
|
185
392
|
|
|
186
|
-
const malformedShape = { ...snapshot,
|
|
393
|
+
const malformedShape = { ...snapshot, classified_minimum: { planning_depth: 'Compact' } };
|
|
187
394
|
const shapeValidation = POLICY.validateWorkflowPolicySnapshot(malformedShape);
|
|
188
395
|
assert.equal(shapeValidation.valid, false);
|
|
189
|
-
assert.match(shapeValidation.errors.join('; '), /
|
|
396
|
+
assert.match(shapeValidation.errors.join('; '), /classified_minimum must contain exactly/);
|
|
190
397
|
|
|
191
398
|
const malformedRisks = { ...snapshot, risks: { auth: true } };
|
|
192
399
|
const risksValidation = POLICY.validateWorkflowPolicySnapshot(malformedRisks);
|
|
@@ -197,26 +404,34 @@ test('P1 persists one strict workflow-policy snapshot and rejects malformed snap
|
|
|
197
404
|
test('P1 workflow-policy validation is semantic and explicit workflow_policy values never reclassify', () => {
|
|
198
405
|
const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
199
406
|
const standardUnknown = POLICY.escalateWorkflowPolicy(direct, { risks: ['unclassified-risk'] });
|
|
200
|
-
const
|
|
201
|
-
const
|
|
407
|
+
const elevatedAuth = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
|
|
408
|
+
const escalatedElevated = POLICY.escalateWorkflowPolicy(direct, { risks: ['auth'] });
|
|
202
409
|
|
|
203
|
-
for (const legitimate of [direct, standardUnknown,
|
|
410
|
+
for (const legitimate of [direct, standardUnknown, elevatedAuth, escalatedElevated]) {
|
|
204
411
|
assert.equal(POLICY.validateWorkflowPolicySnapshot(legitimate).valid, true);
|
|
205
412
|
}
|
|
206
|
-
assert.equal(
|
|
207
|
-
assert.equal(
|
|
413
|
+
assert.equal(escalatedElevated.lane, 'Standard');
|
|
414
|
+
assert.equal(escalatedElevated.automatic_lane, 'Standard');
|
|
415
|
+
assert.equal(escalatedElevated.planning_depth, 'None');
|
|
416
|
+
assert.equal(escalatedElevated.assurance_level, 'Elevated');
|
|
208
417
|
|
|
209
418
|
const forgedDirectAuth = { ...direct, risks: ['auth'] };
|
|
210
419
|
const forgedDirectUnknown = { ...direct, risks: ['unclassified-risk'] };
|
|
211
420
|
const forgedStandardAuth = { ...standardUnknown, risks: ['auth'] };
|
|
212
|
-
for (const forged of [forgedDirectAuth, forgedDirectUnknown
|
|
421
|
+
for (const forged of [forgedDirectAuth, forgedDirectUnknown]) {
|
|
213
422
|
const result = POLICY.validateWorkflowPolicySnapshot(forged);
|
|
214
423
|
assert.equal(result.valid, false);
|
|
215
|
-
assert.match(result.errors.join('; '), /at least (
|
|
424
|
+
assert.match(result.errors.join('; '), /at least (Elevated|Strict)/);
|
|
216
425
|
}
|
|
426
|
+
assert.equal(
|
|
427
|
+
POLICY.validateWorkflowPolicySnapshot(forgedStandardAuth).valid,
|
|
428
|
+
true,
|
|
429
|
+
'changing one normalized risk to another does not invent a higher assurance minimum',
|
|
430
|
+
);
|
|
217
431
|
for (const riskAlias of ['Auth', 'authentication', 'public_contract']) {
|
|
218
432
|
const escalated = POLICY.escalateWorkflowPolicy(direct, { risks: [riskAlias] });
|
|
219
|
-
assert.equal(escalated.lane, '
|
|
433
|
+
assert.equal(escalated.lane, 'Standard', `${riskAlias} must classify as Elevated`);
|
|
434
|
+
assert.equal(escalated.assurance_level, 'Elevated');
|
|
220
435
|
assert.equal(POLICY.validateWorkflowPolicySnapshot({ ...direct, risks: [riskAlias] }).valid, false);
|
|
221
436
|
}
|
|
222
437
|
assert.throws(() => POLICY.assertWorkflowPolicySnapshot(forgedDirectAuth), /Invalid workflow policy snapshot/);
|
|
@@ -278,23 +493,33 @@ test('P1 workflow policy escalation is monotonic across all newly classified ris
|
|
|
278
493
|
|
|
279
494
|
const standard = POLICY.escalateWorkflowPolicy(direct, { risks: ['new-standard-risk'], riskLevel: 'standard' });
|
|
280
495
|
assert.equal(standard.lane, 'Standard');
|
|
281
|
-
assert.equal(standard.artifact_profile, '
|
|
496
|
+
assert.equal(standard.artifact_profile, 'targeted');
|
|
497
|
+
assert.equal(standard.planning_depth, 'None');
|
|
498
|
+
assert.equal(standard.assurance_level, 'Elevated');
|
|
282
499
|
assert.equal(standard.risks.includes('new-standard-risk'), true);
|
|
283
500
|
assert.deepEqual(standard.proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
284
501
|
|
|
285
|
-
const
|
|
286
|
-
assert.equal(
|
|
502
|
+
const ambiguityElevated = POLICY.escalateWorkflowPolicy(direct, { riskSignals: { ambiguity: true } });
|
|
503
|
+
assert.equal(ambiguityElevated.lane, 'Standard', 'risk discovery must require inspection without inventing an audit');
|
|
504
|
+
assert.equal(ambiguityElevated.assurance_level, 'Elevated');
|
|
287
505
|
|
|
288
|
-
const
|
|
506
|
+
const criticalSeverity = POLICY.escalateWorkflowPolicy(direct, { risks: ['payment'], riskLevel: 'critical' });
|
|
507
|
+
assert.equal(criticalSeverity.assurance_level, 'Elevated', 'severity labels must not create independent-audit authority');
|
|
508
|
+
assert.equal(criticalSeverity.proof_obligations.includes('needsIndependentAudit'), false);
|
|
509
|
+
|
|
510
|
+
const critical = POLICY.escalateWorkflowPolicy(standard, { risks: ['auth'], assurance_level: 'Strict' });
|
|
289
511
|
assert.equal(critical.lane, 'Critical');
|
|
290
|
-
assert.equal(critical.artifact_profile, '
|
|
512
|
+
assert.equal(critical.artifact_profile, 'targeted');
|
|
513
|
+
assert.equal(critical.planning_depth, 'None');
|
|
514
|
+
assert.equal(critical.assurance_level, 'Strict');
|
|
515
|
+
assert.equal(critical.automatic_assurance_level, 'Elevated');
|
|
291
516
|
assert.equal(critical.proof_obligations.includes('needsIndependentAudit'), true);
|
|
292
|
-
assert.equal(critical.proof_obligations.includes('needsResearchGrounding'),
|
|
517
|
+
assert.equal(critical.proof_obligations.includes('needsResearchGrounding'), false);
|
|
293
518
|
|
|
294
519
|
const noDowngrade = POLICY.escalateWorkflowPolicy(critical, { risks: ['another-standard-risk'], riskLevel: 'standard' });
|
|
295
520
|
assert.equal(noDowngrade.lane, 'Critical');
|
|
296
521
|
assert.equal(noDowngrade.proof_obligations.includes('needsIndependentAudit'), true);
|
|
297
|
-
assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'),
|
|
522
|
+
assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'), false);
|
|
298
523
|
|
|
299
524
|
const criticalAgain = POLICY.escalateWorkflowPolicy(critical, { risks: ['auth'] });
|
|
300
525
|
assert.deepEqual(criticalAgain, critical, 'repeating known risks must not create a new escalation');
|
|
@@ -316,41 +541,57 @@ test('P1 legacy execution tier is read-compatible without becoming emitted polic
|
|
|
316
541
|
assert.equal(POLICY.workflowPolicySnapshot({ design_context: { execution_tier: 'standard' } }).lane, 'Standard');
|
|
317
542
|
});
|
|
318
543
|
|
|
319
|
-
test('
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
risks: ['
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
544
|
+
test('legacy v1 policy is read-compatible, immutable on read, and explicitly migrates to v2.1', () => {
|
|
545
|
+
const cases = [
|
|
546
|
+
{ lane: 'Direct', risks: [], obligations: ['needsExecutionProof'] },
|
|
547
|
+
{ lane: 'Standard', risks: [], obligations: ['needsInspection', 'needsExecutionProof'] },
|
|
548
|
+
{ lane: 'Critical', risks: ['auth'], obligations: ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding'] },
|
|
549
|
+
];
|
|
550
|
+
for (const fixture of cases) {
|
|
551
|
+
const v1 = {
|
|
552
|
+
version: '1',
|
|
553
|
+
lane: fixture.lane,
|
|
554
|
+
automatic_lane: fixture.lane,
|
|
555
|
+
risks: fixture.risks,
|
|
556
|
+
artifact_profile: { Direct: 'targeted', Standard: 'bounded', Critical: 'strict' }[fixture.lane],
|
|
557
|
+
proof_obligations: fixture.obligations,
|
|
558
|
+
actor_needs: POLICY.actorNeedsFor(fixture.obligations),
|
|
559
|
+
override_receipt: null,
|
|
560
|
+
};
|
|
561
|
+
assert.equal(POLICY.validateWorkflowPolicySnapshot(v1).valid, true, fixture.lane);
|
|
562
|
+
const source = { feature_name: `legacy-${fixture.lane}`, workflow_policy: v1 };
|
|
563
|
+
const before = JSON.stringify(source);
|
|
564
|
+
const adapted = POLICY.readWorkflowPolicySnapshot(source);
|
|
565
|
+
assert.equal(adapted.version, '2');
|
|
566
|
+
assert.deepEqual(adapted.proof_obligations, fixture.obligations, fixture.lane);
|
|
567
|
+
assert.deepEqual(adapted.actor_needs, v1.actor_needs, fixture.lane);
|
|
568
|
+
assert.equal(POLICY.validateWorkflowPolicySnapshot(adapted).valid, true, fixture.lane);
|
|
569
|
+
assert.equal(JSON.stringify(source), before, 'read adapter must not mutate persisted v1 state');
|
|
570
|
+
const migrated = POLICY.persistWorkflowPolicySnapshot(source).workflow_policy;
|
|
571
|
+
assert.equal(migrated.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
572
|
+
assert.deepEqual(Object.keys(migrated).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
573
|
+
const migratedView = POLICY.readWorkflowPolicySnapshot({ workflow_policy: migrated });
|
|
574
|
+
assert.equal(migratedView.lane, adapted.lane, fixture.lane);
|
|
575
|
+
assert.equal(migratedView.artifact_profile, adapted.artifact_profile, fixture.lane);
|
|
576
|
+
assert.deepEqual(migratedView.proof_obligations, POLICY.obligationsForAssurance(
|
|
577
|
+
migrated.assurance_level,
|
|
578
|
+
migrated.risks,
|
|
579
|
+
), fixture.lane);
|
|
580
|
+
}
|
|
346
581
|
});
|
|
347
582
|
|
|
348
|
-
test('
|
|
349
|
-
assert.throws(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
583
|
+
test('post-classification downgrade is unsupported', () => {
|
|
584
|
+
assert.throws(
|
|
585
|
+
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
586
|
+
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test('caller-requested downgrade is blocked before persistence', () => {
|
|
591
|
+
assert.throws(
|
|
592
|
+
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
593
|
+
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
594
|
+
);
|
|
354
595
|
// also via CLI
|
|
355
596
|
const cli = spawnSync(process.execPath, [
|
|
356
597
|
POLICY_PATH,
|
|
@@ -360,7 +601,7 @@ test('unsafe downgrade without user authorization is blocked', () => {
|
|
|
360
601
|
'--json',
|
|
361
602
|
], { encoding: 'utf8' });
|
|
362
603
|
assert.equal(cli.status, 2);
|
|
363
|
-
assert.match(cli.stderr, /
|
|
604
|
+
assert.match(cli.stderr, /planning_depth downgrade.*not permitted/i);
|
|
364
605
|
});
|
|
365
606
|
|
|
366
607
|
test('forged approval via legacy approved field is rejected', () => {
|
|
@@ -380,14 +621,14 @@ test('forged approval via legacy approved field is rejected', () => {
|
|
|
380
621
|
assert.match(legacyCheck.error, /Legacy/);
|
|
381
622
|
});
|
|
382
623
|
|
|
383
|
-
test('
|
|
624
|
+
test('technical approval readiness ignores legacy user_approved', () => {
|
|
384
625
|
const state = POLICY.approvalState({ generated: true, agent_validated: true });
|
|
385
626
|
assert.equal(state.generated, true);
|
|
386
627
|
assert.equal(state.agent_validated, true);
|
|
387
|
-
assert.equal(state.
|
|
388
|
-
assert.equal(state.ready, false);
|
|
628
|
+
assert.equal(state.ready, true);
|
|
389
629
|
assert.equal(state.schema_version, '2.0');
|
|
390
630
|
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true }).ready, true);
|
|
631
|
+
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false }).ready, true);
|
|
391
632
|
const cli = spawnSync(process.execPath, [
|
|
392
633
|
POLICY_PATH,
|
|
393
634
|
'--approval-state',
|
|
@@ -396,7 +637,7 @@ test('approval state never infers user approval from generated or agent validati
|
|
|
396
637
|
'--json',
|
|
397
638
|
], { encoding: 'utf8' });
|
|
398
639
|
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
399
|
-
assert.equal(JSON.parse(cli.stdout).state.
|
|
640
|
+
assert.equal(JSON.parse(cli.stdout).state.ready, true);
|
|
400
641
|
});
|
|
401
642
|
|
|
402
643
|
test('approval schema fails closed for explicit null, array, empty, and malformed states', () => {
|
|
@@ -422,7 +663,7 @@ test('approval schema fails closed for explicit null, array, empty, and malforme
|
|
|
422
663
|
assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
|
|
423
664
|
});
|
|
424
665
|
|
|
425
|
-
test('CLI exposes
|
|
666
|
+
test('CLI exposes a derived lane and Develop consumes independent canonical policy axes', () => {
|
|
426
667
|
const cli = spawnSync(process.execPath, [
|
|
427
668
|
POLICY_PATH,
|
|
428
669
|
'--classify-lane',
|
|
@@ -436,65 +677,59 @@ test('CLI exposes JSON lane classification and develop has no universal spec har
|
|
|
436
677
|
assert.equal(payload.policy.requiresSpec, false);
|
|
437
678
|
const develop = read(DEVELOP);
|
|
438
679
|
assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
680
|
+
const policySection = develop.split('## Policy authority')[1]?.split('\n## ')[0] || '';
|
|
681
|
+
for (const authorityTerm of ['planning_depth', 'assurance_level', 'Risk', 'derived']) {
|
|
682
|
+
assert.ok(policySection.includes(authorityTerm), `policy authority missing ${authorityTerm}`);
|
|
683
|
+
}
|
|
684
|
+
for (const compatibilityView of ['**Direct**', '**Standard**', '**Critical**']) {
|
|
685
|
+
assert.ok(policySection.includes(compatibilityView), `missing derived view ${compatibilityView}`);
|
|
686
|
+
}
|
|
687
|
+
assert.ok(policySection.includes('derived Strict view'));
|
|
688
|
+
assert.ok(policySection.includes('Legacy approval fields'));
|
|
689
|
+
assert.ok(policySection.replace(/\s+/g, ' ').includes('zero authority'));
|
|
444
690
|
assert.match(develop, /Classify lane[\s\S]*test receipt[\s\S]*docs-impact sync/i);
|
|
445
691
|
assert.doesNotMatch(develop, /D2\[Step 3/);
|
|
446
692
|
assert.match(develop, /--notes.*opt-in/i);
|
|
447
693
|
assert.doesNotMatch(develop, /--no-notes/);
|
|
448
694
|
assert.doesNotMatch(develop, /planner|implementer|test-runner|code-auditor|docs-keeper/i);
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
695
|
+
const fullRoutine = POLICY.readWorkflowPolicySnapshot({
|
|
696
|
+
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Routine' }),
|
|
697
|
+
});
|
|
698
|
+
const compactStrict = POLICY.readWorkflowPolicySnapshot({
|
|
699
|
+
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' }),
|
|
700
|
+
});
|
|
701
|
+
assert.equal(fullRoutine.planning_depth, 'Full');
|
|
702
|
+
assert.equal(fullRoutine.assurance_level, 'Routine');
|
|
703
|
+
assert.equal(compactStrict.planning_depth, 'Compact');
|
|
704
|
+
assert.equal(compactStrict.assurance_level, 'Strict');
|
|
705
|
+
assert.notEqual(fullRoutine.artifact_profile, compactStrict.artifact_profile);
|
|
706
|
+
assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
|
|
453
707
|
});
|
|
454
708
|
|
|
455
|
-
test('
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
assert.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
assert.match(specs, /Set `ready_for_implementation = true` only[\s\S]*deterministic spec validation[\s\S]*grounded requirement\/task mappings and contracts/i);
|
|
476
|
-
assert.match(specs, /neither a[\s\S]*canonical execution receipt[\s\S]*independent closeout audit is required yet/i);
|
|
477
|
-
assert.match(specs, /After implementation, feature\/task closeout is a separate gate[\s\S]*canonical execution receipt/i);
|
|
478
|
-
|
|
479
|
-
assert.match(develop, /specific-task mode never selects or chains/i);
|
|
480
|
-
assert.match(develop, /exactly one closeout owner/i);
|
|
481
|
-
assert.match(develop, /test owner[\s\S]*canonical execution receipt/i);
|
|
482
|
-
assert.match(develop, /review owner[\s\S]*never creates or claims execution proof/i);
|
|
483
|
-
assert.match(develop, /docs impact/i);
|
|
484
|
-
assert.match(gate, /no fixed Light\/Standard\/Deep agent sequence/i);
|
|
485
|
-
assert.match(gate, /no\s+blocking Medium finding/i);
|
|
486
|
-
assert.match(gate, /Finding count never selects review depth/i);
|
|
487
|
-
assert.doesNotMatch(gate, /at most one Medium/i);
|
|
488
|
-
assert.match(testSkill, /sole owner of canonical execution proof/i);
|
|
489
|
-
assert.match(review, /does not[\s\S]*canonical execution[\s\S]*receipt/i);
|
|
490
|
-
assert.match(review, /lane, risk, and blast radius/i);
|
|
491
|
-
assert.match(review, /no\s+blocking Medium finding/i);
|
|
492
|
-
assert.match(review, /Finding count never selects depth/i);
|
|
493
|
-
assert.match(specMaker, /no canonical execution receipt[\s\S]*independent closeout audit is required[\s\S]*yet/i);
|
|
494
|
-
assert.match(specMaker, /passing deterministic spec validation/i);
|
|
495
|
-
for (const content of [develop, gate, review]) {
|
|
496
|
-
assert.doesNotMatch(content, /planner|implementer|test-runner|code-auditor|docs-keeper/i);
|
|
709
|
+
test('artifact router keeps optional expansion bound to uncertainty and typed topology', () => {
|
|
710
|
+
const router = markdownSection(read(SPECS), 'Artifact router');
|
|
711
|
+
const rows = markdownTable(router, 'Depth');
|
|
712
|
+
assert.deepEqual(Object.keys(rows), ['None', 'Compact', 'Full']);
|
|
713
|
+
assert.equal(rows.None['Durable core'], 'none');
|
|
714
|
+
for (const depth of ['Compact', 'Full']) {
|
|
715
|
+
assert.deepEqual(
|
|
716
|
+
rows[depth]['Durable core'].match(/`[^`]+`/g),
|
|
717
|
+
['`spec.json`', '`requirements.md`', '`design.md`'],
|
|
718
|
+
`${depth} must share the canonical three-file core`,
|
|
719
|
+
);
|
|
720
|
+
assert.equal(rows[depth]['Research route'], 'only on a research trigger');
|
|
721
|
+
assert.equal(rows[depth]['Task route'], 'only on a typed topology trigger');
|
|
722
|
+
}
|
|
723
|
+
assert.notEqual(rows.Compact['Design expansion'], rows.Full['Design expansion']);
|
|
724
|
+
for (const falseTrigger of ['Requirement count', 'size', 'planning depth', 'assurance level', 'risk label', 'architectural layers']) {
|
|
725
|
+
assert.ok(router.includes(falseTrigger), `router must name inert trigger ${falseTrigger}`);
|
|
726
|
+
}
|
|
727
|
+
for (const boundary of ['ownership', 'dependency', 'transition', 'proof', 'parallel']) {
|
|
728
|
+
assert.ok(router.includes(boundary), `router must name typed topology ${boundary}`);
|
|
497
729
|
}
|
|
730
|
+
assert.ok(router.includes('unresolved material uncertainty'));
|
|
731
|
+
assert.ok(router.includes('external-current fact'));
|
|
732
|
+
assert.ok(router.includes('explicit user request'));
|
|
498
733
|
});
|
|
499
734
|
|
|
500
735
|
test('canonical verdict adapter handles completion and unfinished decisions', () => {
|
|
@@ -525,7 +760,7 @@ test('canonical verdict adapter handles completion and unfinished decisions', ()
|
|
|
525
760
|
|
|
526
761
|
test('completion decision requires canonical execution receipt and every workflow obligation', () => {
|
|
527
762
|
const receipt = canonicalReceipt();
|
|
528
|
-
const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
|
|
763
|
+
const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
|
|
529
764
|
const audit = {
|
|
530
765
|
schema_version: '1',
|
|
531
766
|
reviewer_session_id: 'review-session-1',
|
|
@@ -619,16 +854,201 @@ test('completion decision requires canonical execution receipt and every workflo
|
|
|
619
854
|
assert.ok(artifactBlocked.missingProof.some((item) => item.includes('artifact_hash')));
|
|
620
855
|
});
|
|
621
856
|
|
|
622
|
-
test('
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
857
|
+
test('auto authoring reaches readiness only after promotion, validation, grounding, and current review', () => {
|
|
858
|
+
const fixture = createCanonicalAuthoringFixture();
|
|
859
|
+
try {
|
|
860
|
+
let spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
861
|
+
assert.equal(spec.semantic_model, null);
|
|
862
|
+
spec.ready_for_implementation = true;
|
|
863
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
864
|
+
let validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
865
|
+
assert.ok(validation.errors.some((error) => error.includes('requires explicit promoted machine semantic authority')));
|
|
866
|
+
|
|
867
|
+
spec.ready_for_implementation = false;
|
|
868
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
869
|
+
promoteCanonicalModel(fixture);
|
|
870
|
+
spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
871
|
+
assert.ok(spec.semantic_model && spec.semantic_model.criteria.length === 2);
|
|
872
|
+
spec.ready_for_implementation = true;
|
|
873
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
874
|
+
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
875
|
+
assert.ok(validation.errors.some((error) => error.includes('readiness requires validated')));
|
|
876
|
+
assert.ok(validation.errors.some((error) => error.includes('requires completed semantic review')));
|
|
877
|
+
|
|
878
|
+
spec = makeCanonicalReady(fixture);
|
|
879
|
+
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
880
|
+
assert.deepEqual(validation.errors, [], validation.errors.join('\n'));
|
|
881
|
+
assert.equal(GROUNDER.groundSpec({ specDir: fixture.specDir, root: fixture.root, spec }).errors.length, 0);
|
|
882
|
+
|
|
883
|
+
const specs = read(SPECS);
|
|
884
|
+
const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
|
|
885
|
+
assert.ok(executableBlocks.every((block) => !/hapo:?develop/i.test(block)), 'Specs must never execute Develop');
|
|
886
|
+
const handoff = markdownSection(specs, 'Platform acceptance and handoff');
|
|
887
|
+
assert.ok(handoff.includes('After readiness'));
|
|
888
|
+
assert.ok(handoff.includes('/hapo:develop <feature>'));
|
|
889
|
+
assert.ok(handoff.includes('$hapo-develop <feature>'));
|
|
890
|
+
assert.ok(handoff.includes('must stop in Specs'));
|
|
891
|
+
} finally {
|
|
892
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test('atomic readiness supports one-criterion Routine specs and binds semantic text', () => {
|
|
897
|
+
const fixture = createOrdinaryAuthoringFixture();
|
|
898
|
+
try {
|
|
899
|
+
const authored = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
900
|
+
authored.decisions = [
|
|
901
|
+
{ id: 'Q1', classification: 'repository_fact', statement: 'The runtime entrypoint is src/entry.js.', status: 'grounded', evidence: 'Repository anchor A-D-01 resolves to src/entry.js.' },
|
|
902
|
+
{ id: 'Q2', classification: 'reversible_assumption', statement: 'Use the existing direct module export for this bounded change.', status: 'recorded', evidence: 'Reversal is limited to src/entry.js and does not alter product data.' },
|
|
903
|
+
];
|
|
904
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(authored, null, 2)}\n`);
|
|
905
|
+
const result = READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() });
|
|
906
|
+
assert.equal(result.spec.ready_for_implementation, true);
|
|
907
|
+
assert.equal(result.spec.semantic_model.criteria.length, 1);
|
|
908
|
+
assert.match(result.spec.semantic_model.criteria[0].text, /delete the selected record/);
|
|
909
|
+
assert.deepEqual(VALIDATOR.validateSpec(fixture.specDir).errors, []);
|
|
910
|
+
|
|
911
|
+
const beforeDigest = result.semantic_digest;
|
|
912
|
+
const design = path.join(fixture.specDir, 'design.md');
|
|
913
|
+
const originalDesign = fs.readFileSync(design, 'utf8');
|
|
914
|
+
fs.writeFileSync(design, originalDesign.replace('deleted rather than archived', 'archived rather than deleted'));
|
|
915
|
+
const decisionProjection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
916
|
+
assert.notDeepEqual(decisionProjection.model.design_records, result.spec.semantic_model.design_records);
|
|
917
|
+
const decisionDigest = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: decisionProjection.model });
|
|
918
|
+
assert.equal(decisionDigest.errors.length, 0, decisionDigest.errors.join('\n'));
|
|
919
|
+
assert.notEqual(decisionDigest.digest, beforeDigest);
|
|
920
|
+
fs.writeFileSync(design, originalDesign);
|
|
921
|
+
|
|
922
|
+
const requirements = path.join(fixture.specDir, 'requirements.md');
|
|
923
|
+
fs.writeFileSync(requirements, fs.readFileSync(requirements, 'utf8').replace('delete the selected record', 'archive the selected record'));
|
|
924
|
+
const projection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
925
|
+
assert.notEqual(projection.model.criteria[0].text, result.spec.semantic_model.criteria[0].text);
|
|
926
|
+
const changed = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: projection.model });
|
|
927
|
+
assert.equal(changed.errors.length, 0, changed.errors.join('\n'));
|
|
928
|
+
assert.notEqual(changed.digest, beforeDigest);
|
|
929
|
+
assert.ok(VALIDATOR.validateSpec(fixture.specDir).errors.some((error) => /projection|stale/.test(error)));
|
|
930
|
+
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test('atomic readiness preserves exact bytes on review, grounding, decision, and Strict authority failures', () => {
|
|
934
|
+
const fixture = createOrdinaryAuthoringFixture();
|
|
935
|
+
try {
|
|
936
|
+
const original = fs.readFileSync(fixture.specPath);
|
|
937
|
+
for (const mutate of [
|
|
938
|
+
(review) => ({ ...review, reviewed_criteria: [] }),
|
|
939
|
+
(review) => ({ ...review, counterexamples: [] }),
|
|
940
|
+
]) {
|
|
941
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: mutate(ordinaryReview()) }), /readiness validation/);
|
|
942
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
943
|
+
}
|
|
944
|
+
fs.unlinkSync(path.join(fixture.root, 'src', 'entry.js'));
|
|
945
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /grounding|validation/);
|
|
946
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
947
|
+
fs.writeFileSync(path.join(fixture.root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
948
|
+
|
|
949
|
+
process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME = '1';
|
|
950
|
+
try {
|
|
951
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /injected finalization failure/);
|
|
952
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
953
|
+
} finally { delete process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME; }
|
|
954
|
+
|
|
955
|
+
const blocked = JSON.parse(original);
|
|
956
|
+
blocked.decisions = [{ id: 'Q1', classification: 'user_owned', statement: 'Choose permanent deletion or retention archive behavior.', status: 'unresolved', evidence: null }];
|
|
957
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
958
|
+
const blockedBytes = fs.readFileSync(fixture.specPath);
|
|
959
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /unresolved material user-owned decision/);
|
|
960
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(blockedBytes), true);
|
|
961
|
+
|
|
962
|
+
delete blocked.decisions;
|
|
963
|
+
blocked.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
964
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
965
|
+
const strictBytes = fs.readFileSync(fixture.specPath);
|
|
966
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
|
|
967
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(strictBytes), true);
|
|
968
|
+
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test('Strict atomic readiness succeeds only after current allowlisted host observation', () => {
|
|
972
|
+
const fixture = createOrdinaryAuthoringFixture();
|
|
973
|
+
try {
|
|
974
|
+
const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
975
|
+
spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
976
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
977
|
+
const before = fs.readFileSync(fixture.specPath);
|
|
978
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
|
|
979
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(before), true);
|
|
980
|
+
|
|
981
|
+
const digest = VALIDATOR.computeSemanticDigest21(fixture.specDir, spec);
|
|
982
|
+
assert.equal(digest.errors.length, 0, digest.errors.join('\n'));
|
|
983
|
+
const authorityHome = path.join(fixture.root, 'authority-home');
|
|
984
|
+
const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({ feature_name: 'ordinary', spec_file: 'specs/ordinary/spec.json', semantic_digest: digest.digest, verdict: 'PASS' })}`;
|
|
985
|
+
const observed = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
|
|
986
|
+
cwd: fixture.root,
|
|
987
|
+
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
988
|
+
input: JSON.stringify({ hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'reviewer-1', agent_type: 'code_auditor', last_assistant_message: claim, cwd: fixture.root }),
|
|
989
|
+
encoding: 'utf8',
|
|
990
|
+
});
|
|
991
|
+
assert.equal(observed.status, 0, observed.stderr);
|
|
992
|
+
const reviewFile = path.join(fixture.root, 'review.json');
|
|
993
|
+
fs.writeFileSync(reviewFile, `${JSON.stringify(ordinaryReview())}\n`);
|
|
994
|
+
const finalized = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'), fixture.specDir, '--review-result', reviewFile], {
|
|
995
|
+
cwd: fixture.root,
|
|
996
|
+
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
997
|
+
encoding: 'utf8',
|
|
998
|
+
});
|
|
999
|
+
assert.equal(finalized.status, 0, `${finalized.stdout}\n${finalized.stderr}`);
|
|
1000
|
+
assert.equal(JSON.parse(fs.readFileSync(fixture.specPath, 'utf8')).ready_for_implementation, true);
|
|
1001
|
+
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test('done is a final-state request bound to current semantic model and digest', () => {
|
|
1005
|
+
const fixture = createCanonicalAuthoringFixture();
|
|
1006
|
+
try {
|
|
1007
|
+
promoteCanonicalModel(fixture);
|
|
1008
|
+
const spec = makeCanonicalReady(fixture);
|
|
1009
|
+
spec.status = 'done';
|
|
1010
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1011
|
+
const candidate = {
|
|
1012
|
+
featureDir: fixture.specDir,
|
|
1013
|
+
featureName: 'feature',
|
|
1014
|
+
specFile: fixture.specPath,
|
|
1015
|
+
spec,
|
|
1016
|
+
};
|
|
1017
|
+
const dependencies = () => ({
|
|
1018
|
+
validator: VALIDATOR,
|
|
1019
|
+
grounder: GROUNDER,
|
|
1020
|
+
semanticAuthority: SEMANTIC_AUTHORITY,
|
|
1021
|
+
});
|
|
1022
|
+
const current = FINAL_STATE.validateCanonicalFinalState({
|
|
1023
|
+
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1024
|
+
});
|
|
1025
|
+
assert.equal(current.ok, true, current.reason);
|
|
1026
|
+
assert.equal(current.semanticDigest, spec.validation.semantic_review.semantic_digest);
|
|
1027
|
+
|
|
1028
|
+
const requirementsPath = path.join(fixture.specDir, 'requirements.md');
|
|
1029
|
+
const requirements = fs.readFileSync(requirementsPath, 'utf8');
|
|
1030
|
+
fs.writeFileSync(requirementsPath, requirements.replace('invalid input shall return rejected state', 'invalid input shall return a deterministic rejected state'));
|
|
1031
|
+
const staleDigest = FINAL_STATE.validateCanonicalFinalState({
|
|
1032
|
+
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1033
|
+
});
|
|
1034
|
+
assert.equal(staleDigest.ok, false);
|
|
1035
|
+
assert.match(staleDigest.reason, /stale|differs from Markdown projection/i);
|
|
1036
|
+
|
|
1037
|
+
fs.writeFileSync(requirementsPath, requirements);
|
|
1038
|
+
const staleModel = structuredClone(spec);
|
|
1039
|
+
staleModel.semantic_model.criteria[0].owner = 'A-D-02';
|
|
1040
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(staleModel, null, 2)}\n`);
|
|
1041
|
+
const invalidModel = FINAL_STATE.validateCanonicalFinalState({
|
|
1042
|
+
policy: POLICY,
|
|
1043
|
+
projectRoot: fixture.root,
|
|
1044
|
+
candidate: { ...candidate, spec: staleModel },
|
|
1045
|
+
dependencies,
|
|
1046
|
+
});
|
|
1047
|
+
assert.equal(invalidModel.ok, false);
|
|
1048
|
+
assert.match(invalidModel.reason, /semantic projection|semantic_model|validation failed/i);
|
|
1049
|
+
} finally {
|
|
1050
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
1051
|
+
}
|
|
632
1052
|
});
|
|
633
1053
|
|
|
634
1054
|
test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
|
|
@@ -665,22 +1085,19 @@ test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
|
|
|
665
1085
|
|
|
666
1086
|
test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
|
|
667
1087
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-spec-gate-'));
|
|
668
|
-
const
|
|
669
|
-
fs.
|
|
670
|
-
fs.
|
|
671
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/spec-gate.cjs'), path.join(claude, 'hooks/spec-gate.cjs'));
|
|
672
|
-
fs.copyFileSync(POLICY_PATH, path.join(claude, 'scripts/workflow-policy.cjs'));
|
|
673
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/scripts/provenance.cjs'), path.join(claude, 'scripts/provenance.cjs'));
|
|
674
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'), path.join(claude, 'scripts/spec-resolver.cjs'));
|
|
1088
|
+
const specGate = installClaudeRuntimeClosure(root);
|
|
1089
|
+
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'spec-final-state.cjs')), true);
|
|
1090
|
+
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'validate-spec-output.cjs')), true);
|
|
675
1091
|
fs.mkdirSync(path.join(root, 'specs', 'demo', 'tasks'), { recursive: true });
|
|
676
1092
|
fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
|
|
677
1093
|
status: 'in_progress',
|
|
1094
|
+
current_phase: 'closeout',
|
|
678
1095
|
feature_name: 'demo',
|
|
679
1096
|
task_registry: { 'tasks/task.md': { status: 'done', receipt: 'FLASH_UNVERIFIED' } },
|
|
680
1097
|
}));
|
|
681
1098
|
initFixtureGit(root);
|
|
682
1099
|
try {
|
|
683
|
-
const result = spawnSync(process.execPath, [
|
|
1100
|
+
const result = spawnSync(process.execPath, [specGate], {
|
|
684
1101
|
cwd: root,
|
|
685
1102
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
686
1103
|
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
@@ -698,24 +1115,21 @@ test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
|
|
|
698
1115
|
test('first-run cache bypass is blocked - canonical receipt required even without cache', () => {
|
|
699
1116
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-first-run-'));
|
|
700
1117
|
const claude = path.join(root, '.claude');
|
|
701
|
-
|
|
702
|
-
fs.mkdirSync(path.join(claude, 'scripts'), { recursive: true });
|
|
703
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/spec-gate.cjs'), path.join(claude, 'hooks/spec-gate.cjs'));
|
|
704
|
-
fs.copyFileSync(POLICY_PATH, path.join(claude, 'scripts/workflow-policy.cjs'));
|
|
705
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/scripts/provenance.cjs'), path.join(claude, 'scripts/provenance.cjs'));
|
|
706
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'), path.join(claude, 'scripts/spec-resolver.cjs'));
|
|
1118
|
+
const specGate = installClaudeRuntimeClosure(root);
|
|
707
1119
|
const specDir = path.join(root, 'specs', 'demo', 'tasks');
|
|
708
1120
|
fs.mkdirSync(specDir, { recursive: true });
|
|
709
1121
|
// Done task without canonical receipt (missing Command, provenance)
|
|
710
1122
|
fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
|
|
711
|
-
status: '
|
|
1123
|
+
status: 'done',
|
|
1124
|
+
current_phase: 'closeout',
|
|
712
1125
|
feature_name: 'demo',
|
|
713
|
-
task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
|
|
1126
|
+
task_registry: { 'tasks/task-R1-01-one.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
|
|
714
1127
|
}));
|
|
715
|
-
fs.writeFileSync(path.join(specDir, 'task.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\n```\nnpm test\n```\n');
|
|
1128
|
+
fs.writeFileSync(path.join(specDir, 'task-R1-01-one.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\n```\nnpm test\n```\n');
|
|
716
1129
|
initFixtureGit(root);
|
|
717
1130
|
try {
|
|
718
|
-
|
|
1131
|
+
assert.equal(fs.existsSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')), false);
|
|
1132
|
+
const result = spawnSync(process.execPath, [specGate], {
|
|
719
1133
|
cwd: root,
|
|
720
1134
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
721
1135
|
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
@@ -724,15 +1138,25 @@ test('first-run cache bypass is blocked - canonical receipt required even withou
|
|
|
724
1138
|
assert.equal(result.status, 0);
|
|
725
1139
|
const payload = JSON.parse(result.stdout);
|
|
726
1140
|
assert.equal(payload.decision, 'block', 'first-run without canonical receipt must block');
|
|
727
|
-
assert.match(payload.reason, /tasks\/task\.md/);
|
|
1141
|
+
assert.match(payload.reason, /tasks\/task-R1-01-one\.md/);
|
|
1142
|
+
assert.match(payload.reason, /receipt|command|provenance/i);
|
|
728
1143
|
// Also test that with canonical receipt it passes
|
|
729
|
-
fs.writeFileSync(path.join(specDir, 'task.md'),
|
|
1144
|
+
fs.writeFileSync(path.join(specDir, 'task-R1-01-one.md'), '# Task R1-01: One\n\n**Status:** done\n\n## Outcome\n\nVerified result.\n\n## Scope and Typed Anchors\n\n- **In scope:** focused result\n- **Out of scope:** none\n\n## Changes\n\n- [x] Verify result.\n\n## Acceptance\n\n- Result is verified.\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- **Command:** `pnpm test`\n- **Expected:** focused tests pass\n- **Negative path:** not relevant for fixture\n- **Reachability:** fixture test entrypoint\n');
|
|
1145
|
+
fs.mkdirSync(path.join(root, 'specs', 'demo', 'receipts'), { recursive: true });
|
|
1146
|
+
fs.writeFileSync(
|
|
1147
|
+
path.join(root, 'specs', 'demo', 'receipts', 'task-R1-01-one.md'),
|
|
1148
|
+
bindFixtureReceipt(root, '# Task Receipt\n\nTask: task-R1-01-one.md\nTask path: tasks/task-R1-01-one.md\nVerification: PASS\nCommand: pnpm test\nExit: 0\nResult: PASS\nExpected: focused tests pass\nObserved: focused tests passed\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n```\nnpm test\nPASS\n```\n'),
|
|
1149
|
+
);
|
|
1150
|
+
fs.writeFileSync(
|
|
1151
|
+
path.join(root, 'specs', 'demo', 'feature-receipt.md'),
|
|
1152
|
+
bindFixtureReceipt(root, '# Feature Receipt\n\nFeature: demo\nVerification: PASS\nCommand: pnpm test\nExit: 0\nResult: PASS\nExpected: integrated feature passes\nObserved: integrated feature passed\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n'),
|
|
1153
|
+
);
|
|
730
1154
|
// Clear cache to simulate fresh first-run with valid receipt
|
|
731
1155
|
try { fs.unlinkSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')); } catch {}
|
|
732
1156
|
try { fs.unlinkSync(path.join(__dirname, '..', '.logs', 'spec-gate-last.json')); } catch {}
|
|
733
1157
|
// Need to clear the cache that the hook uses (under claude hooks .logs) - we already cleared above
|
|
734
1158
|
// Re-run with valid receipt - should not block
|
|
735
|
-
const result2 = spawnSync(process.execPath, [
|
|
1159
|
+
const result2 = spawnSync(process.execPath, [specGate], {
|
|
736
1160
|
cwd: root,
|
|
737
1161
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
738
1162
|
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
@@ -986,7 +1410,7 @@ test('runtime provenance derives exact Git evidence, CLI context, and stale/forg
|
|
|
986
1410
|
}
|
|
987
1411
|
});
|
|
988
1412
|
|
|
989
|
-
test('lane traces - Direct, Standard,
|
|
1413
|
+
test('lane traces - Direct, Standard, explicit Strict classification, override, state mutation and completion', () => {
|
|
990
1414
|
// Direct: isolated reversible low-risk
|
|
991
1415
|
const direct = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
|
|
992
1416
|
assert.equal(direct.lane, 'Direct');
|
|
@@ -997,29 +1421,30 @@ test('lane traces - Direct, Standard, Critical classification, override, state m
|
|
|
997
1421
|
// Standard: default
|
|
998
1422
|
const standard = POLICY.classifyLane({ title: 'add pagination to list', taskCount: 1 });
|
|
999
1423
|
assert.equal(standard.lane, 'Standard');
|
|
1000
|
-
assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['
|
|
1424
|
+
assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['needsExecutionProof']);
|
|
1001
1425
|
assert.equal(POLICY.lanePolicy(standard).shipPoint, 'feature');
|
|
1002
|
-
// Critical
|
|
1003
|
-
const
|
|
1426
|
+
// Risk alone is Elevated; Critical requires an explicit Strict assurance choice.
|
|
1427
|
+
const elevated = POLICY.classifyLane({ riskSignals: { auth: true }, taskCount: 1 });
|
|
1428
|
+
assert.equal(elevated.lane, 'Standard');
|
|
1429
|
+
assert.deepEqual(POLICY.lanePolicy(elevated).proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
1430
|
+
const critical = POLICY.classifyLane({ riskSignals: { auth: true }, assurance_level: 'Strict', taskCount: 1 });
|
|
1004
1431
|
assert.equal(critical.lane, 'Critical');
|
|
1005
1432
|
assert.ok(critical.risks.includes('auth'));
|
|
1006
|
-
assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit'
|
|
1433
|
+
assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
1007
1434
|
// Override: Direct requesting Critical is allowed (upgrade) without extra auth
|
|
1008
1435
|
const upgrade = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1, override: 'Critical' });
|
|
1009
1436
|
assert.equal(upgrade.lane, 'Critical');
|
|
1010
1437
|
assert.equal(upgrade.automaticLane, 'Direct');
|
|
1011
1438
|
// Downgrade without auth is blocked
|
|
1012
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /
|
|
1013
|
-
//
|
|
1014
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true }), /
|
|
1015
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', user_approved: true }), /
|
|
1016
|
-
// Even structured receipt without verified issuer is blocked fail-closed
|
|
1017
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', overrideReceipt: { automaticLane: 'Critical', requestedLane: 'Standard', risks: ['payment'], source: 'runtime', timestamp: '2026-08-11T00:00:00.000Z', sessionId: 's-123' } }), /is blocked.*no trusted runtime issuer/);
|
|
1439
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /assurance_level downgrade.*blocked/i);
|
|
1440
|
+
// Caller-owned booleans cannot weaken the classified minimum.
|
|
1441
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true }), /assurance_level downgrade.*blocked/i);
|
|
1442
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', user_approved: true }), /assurance_level downgrade.*blocked/i);
|
|
1018
1443
|
// State mutation: approvalState
|
|
1019
1444
|
const pendingApproval = POLICY.approvalState({ generated: true, agent_validated: false, user_approved: false });
|
|
1020
1445
|
assert.equal(pendingApproval.ready, false);
|
|
1021
1446
|
const agentValidated = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false });
|
|
1022
|
-
assert.equal(agentValidated.ready,
|
|
1447
|
+
assert.equal(agentValidated.ready, true, 'generated plus agent validation is technical readiness');
|
|
1023
1448
|
const approved = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true });
|
|
1024
1449
|
assert.equal(approved.ready, true);
|
|
1025
1450
|
// Completion: flash work remains in_progress and does not unblock
|
|
@@ -1946,24 +2371,29 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
|
|
|
1946
2371
|
}
|
|
1947
2372
|
});
|
|
1948
2373
|
|
|
1949
|
-
test('P0
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
assert.
|
|
1957
|
-
assert.equal(POLICY.isUserAuthorizedForDowngrade
|
|
2374
|
+
test('P0 caller-owned downgrade flags have no canonical capability', () => {
|
|
2375
|
+
for (const callerClaim of [{ user_approved: true }, { userApproved: true }, { userAuthorized: true }]) {
|
|
2376
|
+
assert.throws(
|
|
2377
|
+
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct', ...callerClaim }),
|
|
2378
|
+
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { auth: true }, override: 'Standard', confirmDowngrade: true }), /assurance_level downgrade.*blocked/i);
|
|
2382
|
+
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
1958
2383
|
});
|
|
1959
2384
|
|
|
1960
|
-
test('P0
|
|
1961
|
-
|
|
1962
|
-
assert.equal(POLICY.isValidOverrideReceipt
|
|
1963
|
-
assert.
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2385
|
+
test('P0 canonical policy exposes no caller-owned downgrade authority', () => {
|
|
2386
|
+
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
2387
|
+
assert.equal(POLICY.isValidOverrideReceipt, undefined);
|
|
2388
|
+
assert.equal(POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS.includes('override_' + 'receipt'), false);
|
|
2389
|
+
assert.throws(
|
|
2390
|
+
() => POLICY.classifyLane({
|
|
2391
|
+
riskSignals: { privacy: true },
|
|
2392
|
+
override: 'Direct',
|
|
2393
|
+
overrideReceipt: { verifiedByRuntime: true },
|
|
2394
|
+
}),
|
|
2395
|
+
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2396
|
+
);
|
|
1967
2397
|
});
|
|
1968
2398
|
|
|
1969
2399
|
test('P0 active spec deterministic: multiple active ambiguity and explicit target/path containment', () => {
|
|
@@ -2056,7 +2486,7 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
|
|
|
2056
2486
|
const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
|
|
2057
2487
|
const forged = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct', riskSignals: { auth: true } }), '--json'], { encoding: 'utf8' });
|
|
2058
2488
|
assert.equal(forged.status, 2, 'forged Direct with auth should be blocked via lanePolicy');
|
|
2059
|
-
assert.match(forged.stderr, /
|
|
2489
|
+
assert.match(forged.stderr, /downgrade.*not permitted/i);
|
|
2060
2490
|
const forged2 = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct' }), '--json'], { encoding: 'utf8' });
|
|
2061
2491
|
assert.equal(forged2.status, 2, 'plain Direct without justification should be blocked');
|
|
2062
2492
|
|
|
@@ -2067,7 +2497,7 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
|
|
|
2067
2497
|
assert.throws(() => { 'use strict'; trusted.lane = 'Direct'; }, /read only|Cannot assign|TypeError/);
|
|
2068
2498
|
const paymentTrusted = POLICY.classifyLane({ riskSignals: { payment: true } });
|
|
2069
2499
|
const forgedCopy = { ...paymentTrusted, lane: 'Direct' };
|
|
2070
|
-
assert.
|
|
2500
|
+
assert.equal(POLICY.lanePolicy(forgedCopy).lane, 'Standard', 'compatibility lane is derived from authoritative axes');
|
|
2071
2501
|
});
|
|
2072
2502
|
|
|
2073
2503
|
test('P0 regression: symlink spec/task containment and malformed spec handling', () => {
|