@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.5
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__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +531 -29
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +653 -185
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- 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 +705 -13
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2013 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +5 -2
- 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 +321 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +254 -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 +99 -28
- 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/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +290 -0
- package/src/claude/scripts/spec-ground.cjs +571 -173
- package/src/claude/scripts/spec-readiness.cjs +425 -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 +1594 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3110 -218
- 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 +214 -262
- 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 +35 -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 +61 -38
- 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 +6 -2
- package/src/codex/hooks/privacy-block.cjs +94 -15
- 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,16 @@ 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 AUTHORING_VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
|
|
21
|
+
const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
|
|
15
22
|
const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
|
|
23
|
+
const SPECS = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md');
|
|
24
|
+
const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
|
|
16
25
|
const GATE = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/quality-gate.md');
|
|
17
26
|
const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
|
|
18
27
|
const SYNC_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/sync/SKILL.md');
|
|
@@ -49,6 +58,193 @@ function initFixtureGit(root) {
|
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs') {
|
|
62
|
+
const destinationRoot = path.join(fixtureRoot, '.claude');
|
|
63
|
+
copyClaudeTestRuntime(PACKAGE_ROOT, destinationRoot);
|
|
64
|
+
const hooksRoot = path.join(PACKAGE_ROOT, 'src', 'claude', 'hooks');
|
|
65
|
+
for (const hook of [
|
|
66
|
+
'spec-gate.cjs',
|
|
67
|
+
'completion-authority-check.cjs',
|
|
68
|
+
'completion-authority-state.cjs',
|
|
69
|
+
'semantic-review-authority.cjs',
|
|
70
|
+
]) {
|
|
71
|
+
const target = path.join(destinationRoot, 'hooks', hook);
|
|
72
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
73
|
+
fs.copyFileSync(path.join(hooksRoot, hook), target);
|
|
74
|
+
}
|
|
75
|
+
return path.join(destinationRoot, entry);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function markdownSection(content, heading) {
|
|
79
|
+
const marker = `## ${heading}`;
|
|
80
|
+
const start = content.indexOf(marker);
|
|
81
|
+
assert.notEqual(start, -1, `missing section ${heading}`);
|
|
82
|
+
const rest = content.slice(start + marker.length);
|
|
83
|
+
const end = rest.search(/\n## /);
|
|
84
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function markdownTable(section, firstHeader) {
|
|
88
|
+
const lines = section.split('\n').filter((line) => line.trim().startsWith('|'));
|
|
89
|
+
const cells = (line) => line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
90
|
+
const headerIndex = lines.findIndex((line) => cells(line)[0] === firstHeader);
|
|
91
|
+
assert.notEqual(headerIndex, -1, `missing table headed by ${firstHeader}`);
|
|
92
|
+
const headers = cells(lines[headerIndex]);
|
|
93
|
+
const rows = {};
|
|
94
|
+
for (const line of lines.slice(headerIndex + 2)) {
|
|
95
|
+
const values = cells(line);
|
|
96
|
+
if (values.length !== headers.length) break;
|
|
97
|
+
rows[values[0].replaceAll('`', '')] = Object.fromEntries(headers.map((header, index) => [header, values[index]]));
|
|
98
|
+
}
|
|
99
|
+
return rows;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runNode(root, script, args) {
|
|
103
|
+
return spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function commandOutput(result) {
|
|
107
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createCanonicalAuthoringFixture() {
|
|
111
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-develop-contract-'));
|
|
112
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
113
|
+
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
114
|
+
fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
|
|
115
|
+
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { enabled: true };\n');
|
|
116
|
+
fs.writeFileSync(path.join(root, 'test', 'service.test.js'), 'require("node:test")("service", () => {});\n');
|
|
117
|
+
const scaffold = runNode(root, SCAFFOLD, ['feature']);
|
|
118
|
+
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
119
|
+
const specDir = path.join(root, 'specs', 'feature');
|
|
120
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
121
|
+
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
### Requirement 1: Observable service behavior
|
|
125
|
+
- **R1.1**: When valid input arrives, the service shall return enabled state; invalid input shall return rejected state.
|
|
126
|
+
- **R1.2**: The verifier shall produce separate observable proof for enabled and rejected states.
|
|
127
|
+
`);
|
|
128
|
+
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
129
|
+
|
|
130
|
+
## Architecture
|
|
131
|
+
The entrypoint exposes the bounded service behavior and a separate proof artifact.
|
|
132
|
+
|
|
133
|
+
## Typed Anchors
|
|
134
|
+
| ID | Type | Target | Role | Access | Action |
|
|
135
|
+
|---|---|---|---|---|---|
|
|
136
|
+
| A-D-01 | file | \`src/entry.js\` | service boundary | read | read |
|
|
137
|
+
| A-D-02 | artifact | \`artifacts/design-proof.json\` | proof evidence | write | create |
|
|
138
|
+
|
|
139
|
+
## Canonical Contracts & Invariants
|
|
140
|
+
### D1 — Dispatch decision
|
|
141
|
+
The entrypoint dispatches exactly once.
|
|
142
|
+
### I1 — Invalid-state invariant
|
|
143
|
+
Invalid input never returns enabled state.
|
|
144
|
+
### C1 — Result contract
|
|
145
|
+
The result contains one enabled boolean.
|
|
146
|
+
|
|
147
|
+
## Verification Definitions
|
|
148
|
+
- **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.
|
|
149
|
+
`);
|
|
150
|
+
return { root, specDir, specPath };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function createOrdinaryAuthoringFixture() {
|
|
154
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-ordinary-readiness-'));
|
|
155
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
156
|
+
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
157
|
+
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
158
|
+
fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), 'require("node:test")("entry", () => {});\n');
|
|
159
|
+
const scaffold = runNode(root, SCAFFOLD, ['ordinary']);
|
|
160
|
+
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
161
|
+
const specDir = path.join(root, 'specs', 'ordinary');
|
|
162
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
163
|
+
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
164
|
+
|
|
165
|
+
## Requirements
|
|
166
|
+
### Requirement 1: Delete behavior
|
|
167
|
+
- **R1.1**: When deletion is requested, the service shall delete the selected record and report deleted state.
|
|
168
|
+
`);
|
|
169
|
+
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
170
|
+
|
|
171
|
+
## Typed Anchors
|
|
172
|
+
| ID | Type | Target | Role | Access | Action |
|
|
173
|
+
|---|---|---|---|---|---|
|
|
174
|
+
| A-D-01 | file | \`src/entry.js\` | behavior owner | read | read |
|
|
175
|
+
|
|
176
|
+
## Canonical Contracts & Invariants
|
|
177
|
+
### D1 — Delete decision
|
|
178
|
+
The selected record is deleted rather than archived.
|
|
179
|
+
### I1 — State invariant
|
|
180
|
+
Deleted state is reported only after deletion succeeds.
|
|
181
|
+
### C1 — Result contract
|
|
182
|
+
The result contains the literal deleted state.
|
|
183
|
+
|
|
184
|
+
## Verification Definitions
|
|
185
|
+
- **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.
|
|
186
|
+
`);
|
|
187
|
+
const semantic = runNode(root, SCAFFOLD, ['ordinary', '--sync-semantic-model']);
|
|
188
|
+
assert.equal(semantic.status, 0, commandOutput(semantic));
|
|
189
|
+
const authoring = runNode(root, AUTHORING_VALIDATOR, [specDir]);
|
|
190
|
+
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
191
|
+
initFixtureGit(root);
|
|
192
|
+
return { root, specDir, specPath };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ordinaryReview() {
|
|
196
|
+
return canonicalPassReview(['R1.1'], [{
|
|
197
|
+
criterion: 'R1.1', case_kind: 'failure',
|
|
198
|
+
scenario: 'Deletion targets a record that does not exist in the current store.',
|
|
199
|
+
expected: 'The service returns not-found and never reports the record as deleted.',
|
|
200
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
201
|
+
}]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function canonicalPassReview(reviewedCriteria, counterexamples) {
|
|
205
|
+
return {
|
|
206
|
+
verdict: 'PASS',
|
|
207
|
+
findings: [],
|
|
208
|
+
unresolved_decisions: [],
|
|
209
|
+
graph_coverage: [
|
|
210
|
+
'criterion_local', 'cross_criterion', 'runtime_path',
|
|
211
|
+
'assumption_provenance', 'compatibility_migration',
|
|
212
|
+
].map((surface) => ({
|
|
213
|
+
surface, covered: true,
|
|
214
|
+
notes: 'The review covers this semantic surface against the canonical model.',
|
|
215
|
+
})),
|
|
216
|
+
reviewed_criteria: reviewedCriteria,
|
|
217
|
+
counterexamples,
|
|
218
|
+
reviewer_evidence: null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function promoteCanonicalModel(fixture) {
|
|
223
|
+
const result = runNode(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
|
|
224
|
+
assert.equal(result.status, 0, commandOutput(result));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function makeCanonicalReady(fixture) {
|
|
228
|
+
const authoring = runNode(fixture.root, AUTHORING_VALIDATOR, [fixture.specDir]);
|
|
229
|
+
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
230
|
+
const result = READINESS.finalizeReadiness({
|
|
231
|
+
specDir: fixture.specDir,
|
|
232
|
+
projectRoot: fixture.root,
|
|
233
|
+
reviewResult: canonicalPassReview(['R1.1', 'R1.2'], [{
|
|
234
|
+
criterion: 'R1.1', case_kind: 'failure',
|
|
235
|
+
scenario: 'The service receives invalid input without its required discriminator.',
|
|
236
|
+
expected: 'The service returns rejected state and never emits enabled true.',
|
|
237
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
238
|
+
}, {
|
|
239
|
+
criterion: 'R1.2', case_kind: 'adversarial',
|
|
240
|
+
scenario: 'The service reports enabled state without producing separate proof evidence.',
|
|
241
|
+
expected: 'Verification fails because the independent proof artifact is absent.',
|
|
242
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
243
|
+
}]),
|
|
244
|
+
});
|
|
245
|
+
return result.spec;
|
|
246
|
+
}
|
|
247
|
+
|
|
52
248
|
function bindFixtureReceipt(root, value, feature = 'demo', session = 'test') {
|
|
53
249
|
const context = PROVENANCE_HELPER.deriveRuntimeContext({
|
|
54
250
|
projectRoot: root,
|
|
@@ -121,7 +317,7 @@ test('delegation plan consumes legacy tiers as obligations without an agent chai
|
|
|
121
317
|
const standard = POLICY.delegationPlan({ tier: 'Standard', mode: 'specific-task' });
|
|
122
318
|
const deep = POLICY.delegationPlan({ tier: 'Deep', mode: 'full-spec', taskCount: 2 });
|
|
123
319
|
assert.deepEqual(light.proof_obligations, ['needsExecutionProof']);
|
|
124
|
-
assert.deepEqual(standard.proof_obligations, ['
|
|
320
|
+
assert.deepEqual(standard.proof_obligations, ['needsExecutionProof']);
|
|
125
321
|
assert.deepEqual(deep.proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
126
322
|
for (const plan of [light, standard, deep]) {
|
|
127
323
|
assert.equal(Object.hasOwn(plan, 'delegated'), false);
|
|
@@ -144,31 +340,53 @@ test('lane classifier selects Direct for explicit reversible low-risk work', ()
|
|
|
144
340
|
assert.equal(policy.proof_obligations.includes('needsDurableTaskState'), false);
|
|
145
341
|
});
|
|
146
342
|
|
|
147
|
-
test('lane classifier
|
|
343
|
+
test('lane classifier raises named risks to Elevated without inventing independent-audit authority', () => {
|
|
148
344
|
const result = POLICY.classifyLane({
|
|
149
345
|
reversible: true,
|
|
150
346
|
riskSignals: { auth: true, migration: true, publicContract: true },
|
|
151
347
|
});
|
|
152
|
-
assert.equal(result.lane, '
|
|
348
|
+
assert.equal(result.lane, 'Standard');
|
|
349
|
+
assert.equal(result.assuranceLevel, 'Elevated');
|
|
350
|
+
assert.equal(result.automaticAssuranceLevel, 'Elevated');
|
|
153
351
|
assert.deepEqual(result.risks, ['auth', 'migration', 'publicContract']);
|
|
154
|
-
assert.deepEqual(POLICY.
|
|
155
|
-
'needsInspection', 'needsExecutionProof',
|
|
352
|
+
assert.deepEqual(POLICY.lanePolicy(result).proof_obligations, [
|
|
353
|
+
'needsInspection', 'needsExecutionProof',
|
|
156
354
|
]);
|
|
355
|
+
|
|
356
|
+
const strict = POLICY.classifyLane({
|
|
357
|
+
riskSignals: { auth: true },
|
|
358
|
+
assurance_level: 'Strict',
|
|
359
|
+
});
|
|
360
|
+
assert.equal(strict.lane, 'Critical');
|
|
361
|
+
assert.equal(strict.automaticLane, 'Standard');
|
|
362
|
+
assert.equal(strict.assuranceLevel, 'Strict');
|
|
363
|
+
assert.equal(strict.automaticAssuranceLevel, 'Elevated');
|
|
364
|
+
assert.ok(POLICY.lanePolicy(strict).proof_obligations.includes('needsIndependentAudit'));
|
|
157
365
|
});
|
|
158
366
|
|
|
159
|
-
test('P1 persists
|
|
367
|
+
test('P1 persists the minimal v2.1 policy and derives compatibility views', () => {
|
|
160
368
|
const initial = POLICY.persistWorkflowPolicySnapshot(
|
|
161
|
-
{ feature_name: 'bounded'
|
|
369
|
+
{ feature_name: 'bounded' },
|
|
162
370
|
{ riskSignals: {} },
|
|
163
371
|
);
|
|
164
372
|
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
|
-
|
|
373
|
+
assert.deepEqual(Object.keys(snapshot).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
374
|
+
assert.equal(snapshot.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
375
|
+
assert.equal(snapshot.planning_depth, 'Compact');
|
|
376
|
+
assert.equal(snapshot.assurance_level, 'Routine');
|
|
377
|
+
assert.deepEqual(snapshot.classified_minimum, {
|
|
378
|
+
planning_depth: 'Compact',
|
|
379
|
+
assurance_level: 'Routine',
|
|
380
|
+
});
|
|
381
|
+
for (const derivedField of ['lane', 'automatic_lane', 'artifact_profile', 'proof_obligations', 'actor_needs']) {
|
|
382
|
+
assert.equal(Object.hasOwn(snapshot, derivedField), false, `${derivedField} must not be persisted`);
|
|
383
|
+
}
|
|
384
|
+
const view = POLICY.readWorkflowPolicySnapshot(initial);
|
|
385
|
+
assert.equal(view.lane, 'Standard');
|
|
386
|
+
assert.equal(view.automatic_lane, 'Standard');
|
|
387
|
+
assert.equal(view.artifact_profile, 'bounded');
|
|
388
|
+
assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
|
|
389
|
+
assert.equal(Object.hasOwn(initial, 'override_' + 'receipt'), false);
|
|
172
390
|
assert.equal(POLICY.validateWorkflowPolicySnapshot(snapshot).valid, true);
|
|
173
391
|
|
|
174
392
|
const persistedAgain = POLICY.persistWorkflowPolicySnapshot(initial, {
|
|
@@ -177,16 +395,16 @@ test('P1 persists one strict workflow-policy snapshot and rejects malformed snap
|
|
|
177
395
|
});
|
|
178
396
|
assert.deepEqual(persistedAgain.workflow_policy, snapshot, 'persist-once must not reclassify an existing snapshot');
|
|
179
397
|
|
|
180
|
-
const malformed = { ...snapshot,
|
|
398
|
+
const malformed = { ...snapshot, lane: 'Critical' };
|
|
181
399
|
const validation = POLICY.validateWorkflowPolicySnapshot(malformed);
|
|
182
400
|
assert.equal(validation.valid, false);
|
|
183
|
-
assert.match(validation.errors.join('; '), /fields must be exactly
|
|
401
|
+
assert.match(validation.errors.join('; '), /v2\.1 fields must be exactly/);
|
|
184
402
|
assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: malformed }), /Invalid workflow policy snapshot/);
|
|
185
403
|
|
|
186
|
-
const malformedShape = { ...snapshot,
|
|
404
|
+
const malformedShape = { ...snapshot, classified_minimum: { planning_depth: 'Compact' } };
|
|
187
405
|
const shapeValidation = POLICY.validateWorkflowPolicySnapshot(malformedShape);
|
|
188
406
|
assert.equal(shapeValidation.valid, false);
|
|
189
|
-
assert.match(shapeValidation.errors.join('; '), /
|
|
407
|
+
assert.match(shapeValidation.errors.join('; '), /classified_minimum must contain exactly/);
|
|
190
408
|
|
|
191
409
|
const malformedRisks = { ...snapshot, risks: { auth: true } };
|
|
192
410
|
const risksValidation = POLICY.validateWorkflowPolicySnapshot(malformedRisks);
|
|
@@ -197,26 +415,34 @@ test('P1 persists one strict workflow-policy snapshot and rejects malformed snap
|
|
|
197
415
|
test('P1 workflow-policy validation is semantic and explicit workflow_policy values never reclassify', () => {
|
|
198
416
|
const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
199
417
|
const standardUnknown = POLICY.escalateWorkflowPolicy(direct, { risks: ['unclassified-risk'] });
|
|
200
|
-
const
|
|
201
|
-
const
|
|
418
|
+
const elevatedAuth = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
|
|
419
|
+
const escalatedElevated = POLICY.escalateWorkflowPolicy(direct, { risks: ['auth'] });
|
|
202
420
|
|
|
203
|
-
for (const legitimate of [direct, standardUnknown,
|
|
421
|
+
for (const legitimate of [direct, standardUnknown, elevatedAuth, escalatedElevated]) {
|
|
204
422
|
assert.equal(POLICY.validateWorkflowPolicySnapshot(legitimate).valid, true);
|
|
205
423
|
}
|
|
206
|
-
assert.equal(
|
|
207
|
-
assert.equal(
|
|
424
|
+
assert.equal(escalatedElevated.lane, 'Standard');
|
|
425
|
+
assert.equal(escalatedElevated.automatic_lane, 'Standard');
|
|
426
|
+
assert.equal(escalatedElevated.planning_depth, 'None');
|
|
427
|
+
assert.equal(escalatedElevated.assurance_level, 'Elevated');
|
|
208
428
|
|
|
209
429
|
const forgedDirectAuth = { ...direct, risks: ['auth'] };
|
|
210
430
|
const forgedDirectUnknown = { ...direct, risks: ['unclassified-risk'] };
|
|
211
431
|
const forgedStandardAuth = { ...standardUnknown, risks: ['auth'] };
|
|
212
|
-
for (const forged of [forgedDirectAuth, forgedDirectUnknown
|
|
432
|
+
for (const forged of [forgedDirectAuth, forgedDirectUnknown]) {
|
|
213
433
|
const result = POLICY.validateWorkflowPolicySnapshot(forged);
|
|
214
434
|
assert.equal(result.valid, false);
|
|
215
|
-
assert.match(result.errors.join('; '), /at least (
|
|
435
|
+
assert.match(result.errors.join('; '), /at least (Elevated|Strict)/);
|
|
216
436
|
}
|
|
437
|
+
assert.equal(
|
|
438
|
+
POLICY.validateWorkflowPolicySnapshot(forgedStandardAuth).valid,
|
|
439
|
+
true,
|
|
440
|
+
'changing one normalized risk to another does not invent a higher assurance minimum',
|
|
441
|
+
);
|
|
217
442
|
for (const riskAlias of ['Auth', 'authentication', 'public_contract']) {
|
|
218
443
|
const escalated = POLICY.escalateWorkflowPolicy(direct, { risks: [riskAlias] });
|
|
219
|
-
assert.equal(escalated.lane, '
|
|
444
|
+
assert.equal(escalated.lane, 'Standard', `${riskAlias} must classify as Elevated`);
|
|
445
|
+
assert.equal(escalated.assurance_level, 'Elevated');
|
|
220
446
|
assert.equal(POLICY.validateWorkflowPolicySnapshot({ ...direct, risks: [riskAlias] }).valid, false);
|
|
221
447
|
}
|
|
222
448
|
assert.throws(() => POLICY.assertWorkflowPolicySnapshot(forgedDirectAuth), /Invalid workflow policy snapshot/);
|
|
@@ -278,23 +504,33 @@ test('P1 workflow policy escalation is monotonic across all newly classified ris
|
|
|
278
504
|
|
|
279
505
|
const standard = POLICY.escalateWorkflowPolicy(direct, { risks: ['new-standard-risk'], riskLevel: 'standard' });
|
|
280
506
|
assert.equal(standard.lane, 'Standard');
|
|
281
|
-
assert.equal(standard.artifact_profile, '
|
|
507
|
+
assert.equal(standard.artifact_profile, 'targeted');
|
|
508
|
+
assert.equal(standard.planning_depth, 'None');
|
|
509
|
+
assert.equal(standard.assurance_level, 'Elevated');
|
|
282
510
|
assert.equal(standard.risks.includes('new-standard-risk'), true);
|
|
283
511
|
assert.deepEqual(standard.proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
284
512
|
|
|
285
|
-
const
|
|
286
|
-
assert.equal(
|
|
513
|
+
const ambiguityElevated = POLICY.escalateWorkflowPolicy(direct, { riskSignals: { ambiguity: true } });
|
|
514
|
+
assert.equal(ambiguityElevated.lane, 'Standard', 'risk discovery must require inspection without inventing an audit');
|
|
515
|
+
assert.equal(ambiguityElevated.assurance_level, 'Elevated');
|
|
287
516
|
|
|
288
|
-
const
|
|
517
|
+
const criticalSeverity = POLICY.escalateWorkflowPolicy(direct, { risks: ['payment'], riskLevel: 'critical' });
|
|
518
|
+
assert.equal(criticalSeverity.assurance_level, 'Elevated', 'severity labels must not create independent-audit authority');
|
|
519
|
+
assert.equal(criticalSeverity.proof_obligations.includes('needsIndependentAudit'), false);
|
|
520
|
+
|
|
521
|
+
const critical = POLICY.escalateWorkflowPolicy(standard, { risks: ['auth'], assurance_level: 'Strict' });
|
|
289
522
|
assert.equal(critical.lane, 'Critical');
|
|
290
|
-
assert.equal(critical.artifact_profile, '
|
|
523
|
+
assert.equal(critical.artifact_profile, 'targeted');
|
|
524
|
+
assert.equal(critical.planning_depth, 'None');
|
|
525
|
+
assert.equal(critical.assurance_level, 'Strict');
|
|
526
|
+
assert.equal(critical.automatic_assurance_level, 'Elevated');
|
|
291
527
|
assert.equal(critical.proof_obligations.includes('needsIndependentAudit'), true);
|
|
292
|
-
assert.equal(critical.proof_obligations.includes('needsResearchGrounding'),
|
|
528
|
+
assert.equal(critical.proof_obligations.includes('needsResearchGrounding'), false);
|
|
293
529
|
|
|
294
530
|
const noDowngrade = POLICY.escalateWorkflowPolicy(critical, { risks: ['another-standard-risk'], riskLevel: 'standard' });
|
|
295
531
|
assert.equal(noDowngrade.lane, 'Critical');
|
|
296
532
|
assert.equal(noDowngrade.proof_obligations.includes('needsIndependentAudit'), true);
|
|
297
|
-
assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'),
|
|
533
|
+
assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'), false);
|
|
298
534
|
|
|
299
535
|
const criticalAgain = POLICY.escalateWorkflowPolicy(critical, { risks: ['auth'] });
|
|
300
536
|
assert.deepEqual(criticalAgain, critical, 'repeating known risks must not create a new escalation');
|
|
@@ -316,41 +552,57 @@ test('P1 legacy execution tier is read-compatible without becoming emitted polic
|
|
|
316
552
|
assert.equal(POLICY.workflowPolicySnapshot({ design_context: { execution_tier: 'standard' } }).lane, 'Standard');
|
|
317
553
|
});
|
|
318
554
|
|
|
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
|
-
|
|
555
|
+
test('legacy v1 policy is read-compatible, immutable on read, and explicitly migrates to v2.1', () => {
|
|
556
|
+
const cases = [
|
|
557
|
+
{ lane: 'Direct', risks: [], obligations: ['needsExecutionProof'] },
|
|
558
|
+
{ lane: 'Standard', risks: [], obligations: ['needsInspection', 'needsExecutionProof'] },
|
|
559
|
+
{ lane: 'Critical', risks: ['auth'], obligations: ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding'] },
|
|
560
|
+
];
|
|
561
|
+
for (const fixture of cases) {
|
|
562
|
+
const v1 = {
|
|
563
|
+
version: '1',
|
|
564
|
+
lane: fixture.lane,
|
|
565
|
+
automatic_lane: fixture.lane,
|
|
566
|
+
risks: fixture.risks,
|
|
567
|
+
artifact_profile: { Direct: 'targeted', Standard: 'bounded', Critical: 'strict' }[fixture.lane],
|
|
568
|
+
proof_obligations: fixture.obligations,
|
|
569
|
+
actor_needs: POLICY.actorNeedsFor(fixture.obligations),
|
|
570
|
+
override_receipt: null,
|
|
571
|
+
};
|
|
572
|
+
assert.equal(POLICY.validateWorkflowPolicySnapshot(v1).valid, true, fixture.lane);
|
|
573
|
+
const source = { feature_name: `legacy-${fixture.lane}`, workflow_policy: v1 };
|
|
574
|
+
const before = JSON.stringify(source);
|
|
575
|
+
const adapted = POLICY.readWorkflowPolicySnapshot(source);
|
|
576
|
+
assert.equal(adapted.version, '2');
|
|
577
|
+
assert.deepEqual(adapted.proof_obligations, fixture.obligations, fixture.lane);
|
|
578
|
+
assert.deepEqual(adapted.actor_needs, v1.actor_needs, fixture.lane);
|
|
579
|
+
assert.equal(POLICY.validateWorkflowPolicySnapshot(adapted).valid, true, fixture.lane);
|
|
580
|
+
assert.equal(JSON.stringify(source), before, 'read adapter must not mutate persisted v1 state');
|
|
581
|
+
const migrated = POLICY.persistWorkflowPolicySnapshot(source).workflow_policy;
|
|
582
|
+
assert.equal(migrated.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
583
|
+
assert.deepEqual(Object.keys(migrated).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
584
|
+
const migratedView = POLICY.readWorkflowPolicySnapshot({ workflow_policy: migrated });
|
|
585
|
+
assert.equal(migratedView.lane, adapted.lane, fixture.lane);
|
|
586
|
+
assert.equal(migratedView.artifact_profile, adapted.artifact_profile, fixture.lane);
|
|
587
|
+
assert.deepEqual(migratedView.proof_obligations, POLICY.obligationsForAssurance(
|
|
588
|
+
migrated.assurance_level,
|
|
589
|
+
migrated.risks,
|
|
590
|
+
), fixture.lane);
|
|
591
|
+
}
|
|
346
592
|
});
|
|
347
593
|
|
|
348
|
-
test('
|
|
349
|
-
assert.throws(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
594
|
+
test('post-classification downgrade is unsupported', () => {
|
|
595
|
+
assert.throws(
|
|
596
|
+
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
597
|
+
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('caller-requested downgrade is blocked before persistence', () => {
|
|
602
|
+
assert.throws(
|
|
603
|
+
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
604
|
+
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
605
|
+
);
|
|
354
606
|
// also via CLI
|
|
355
607
|
const cli = spawnSync(process.execPath, [
|
|
356
608
|
POLICY_PATH,
|
|
@@ -360,7 +612,7 @@ test('unsafe downgrade without user authorization is blocked', () => {
|
|
|
360
612
|
'--json',
|
|
361
613
|
], { encoding: 'utf8' });
|
|
362
614
|
assert.equal(cli.status, 2);
|
|
363
|
-
assert.match(cli.stderr, /
|
|
615
|
+
assert.match(cli.stderr, /planning_depth downgrade.*not permitted/i);
|
|
364
616
|
});
|
|
365
617
|
|
|
366
618
|
test('forged approval via legacy approved field is rejected', () => {
|
|
@@ -380,14 +632,14 @@ test('forged approval via legacy approved field is rejected', () => {
|
|
|
380
632
|
assert.match(legacyCheck.error, /Legacy/);
|
|
381
633
|
});
|
|
382
634
|
|
|
383
|
-
test('
|
|
635
|
+
test('technical approval readiness ignores legacy user_approved', () => {
|
|
384
636
|
const state = POLICY.approvalState({ generated: true, agent_validated: true });
|
|
385
637
|
assert.equal(state.generated, true);
|
|
386
638
|
assert.equal(state.agent_validated, true);
|
|
387
|
-
assert.equal(state.
|
|
388
|
-
assert.equal(state.ready, false);
|
|
639
|
+
assert.equal(state.ready, true);
|
|
389
640
|
assert.equal(state.schema_version, '2.0');
|
|
390
641
|
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true }).ready, true);
|
|
642
|
+
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false }).ready, true);
|
|
391
643
|
const cli = spawnSync(process.execPath, [
|
|
392
644
|
POLICY_PATH,
|
|
393
645
|
'--approval-state',
|
|
@@ -396,7 +648,7 @@ test('approval state never infers user approval from generated or agent validati
|
|
|
396
648
|
'--json',
|
|
397
649
|
], { encoding: 'utf8' });
|
|
398
650
|
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
399
|
-
assert.equal(JSON.parse(cli.stdout).state.
|
|
651
|
+
assert.equal(JSON.parse(cli.stdout).state.ready, true);
|
|
400
652
|
});
|
|
401
653
|
|
|
402
654
|
test('approval schema fails closed for explicit null, array, empty, and malformed states', () => {
|
|
@@ -422,7 +674,7 @@ test('approval schema fails closed for explicit null, array, empty, and malforme
|
|
|
422
674
|
assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
|
|
423
675
|
});
|
|
424
676
|
|
|
425
|
-
test('CLI exposes
|
|
677
|
+
test('CLI exposes a derived lane and Develop consumes independent canonical policy axes', () => {
|
|
426
678
|
const cli = spawnSync(process.execPath, [
|
|
427
679
|
POLICY_PATH,
|
|
428
680
|
'--classify-lane',
|
|
@@ -436,65 +688,59 @@ test('CLI exposes JSON lane classification and develop has no universal spec har
|
|
|
436
688
|
assert.equal(payload.policy.requiresSpec, false);
|
|
437
689
|
const develop = read(DEVELOP);
|
|
438
690
|
assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
691
|
+
const policySection = develop.split('## Policy authority')[1]?.split('\n## ')[0] || '';
|
|
692
|
+
for (const authorityTerm of ['planning_depth', 'assurance_level', 'Risk', 'derived']) {
|
|
693
|
+
assert.ok(policySection.includes(authorityTerm), `policy authority missing ${authorityTerm}`);
|
|
694
|
+
}
|
|
695
|
+
for (const compatibilityView of ['**Direct**', '**Standard**', '**Critical**']) {
|
|
696
|
+
assert.ok(policySection.includes(compatibilityView), `missing derived view ${compatibilityView}`);
|
|
697
|
+
}
|
|
698
|
+
assert.ok(policySection.includes('derived Strict view'));
|
|
699
|
+
assert.ok(policySection.includes('Legacy approval fields'));
|
|
700
|
+
assert.ok(policySection.replace(/\s+/g, ' ').includes('zero authority'));
|
|
444
701
|
assert.match(develop, /Classify lane[\s\S]*test receipt[\s\S]*docs-impact sync/i);
|
|
445
702
|
assert.doesNotMatch(develop, /D2\[Step 3/);
|
|
446
703
|
assert.match(develop, /--notes.*opt-in/i);
|
|
447
704
|
assert.doesNotMatch(develop, /--no-notes/);
|
|
448
705
|
assert.doesNotMatch(develop, /planner|implementer|test-runner|code-auditor|docs-keeper/i);
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
706
|
+
const fullRoutine = POLICY.readWorkflowPolicySnapshot({
|
|
707
|
+
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Routine' }),
|
|
708
|
+
});
|
|
709
|
+
const compactStrict = POLICY.readWorkflowPolicySnapshot({
|
|
710
|
+
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' }),
|
|
711
|
+
});
|
|
712
|
+
assert.equal(fullRoutine.planning_depth, 'Full');
|
|
713
|
+
assert.equal(fullRoutine.assurance_level, 'Routine');
|
|
714
|
+
assert.equal(compactStrict.planning_depth, 'Compact');
|
|
715
|
+
assert.equal(compactStrict.assurance_level, 'Strict');
|
|
716
|
+
assert.notEqual(fullRoutine.artifact_profile, compactStrict.artifact_profile);
|
|
717
|
+
assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
|
|
453
718
|
});
|
|
454
719
|
|
|
455
|
-
test('
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
assert.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
assert.match(specs, /spec-ready[\s\S]*PENDING/i);
|
|
473
|
-
assert.match(specs, /canonical execution receipt/i);
|
|
474
|
-
assert.match(specs, /Audit: PASS.*not.*evidence/i);
|
|
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);
|
|
720
|
+
test('artifact router keeps optional expansion bound to uncertainty and typed topology', () => {
|
|
721
|
+
const router = markdownSection(read(SPECS), 'Artifact router');
|
|
722
|
+
const rows = markdownTable(router, 'Depth');
|
|
723
|
+
assert.deepEqual(Object.keys(rows), ['None', 'Compact', 'Full']);
|
|
724
|
+
assert.equal(rows.None['Durable core'], 'none');
|
|
725
|
+
for (const depth of ['Compact', 'Full']) {
|
|
726
|
+
assert.deepEqual(
|
|
727
|
+
rows[depth]['Durable core'].match(/`[^`]+`/g),
|
|
728
|
+
['`spec.json`', '`requirements.md`', '`design.md`'],
|
|
729
|
+
`${depth} must share the canonical three-file core`,
|
|
730
|
+
);
|
|
731
|
+
assert.equal(rows[depth]['Research route'], 'only on a research trigger');
|
|
732
|
+
assert.equal(rows[depth]['Task route'], 'only on a typed topology trigger');
|
|
733
|
+
}
|
|
734
|
+
assert.notEqual(rows.Compact['Design expansion'], rows.Full['Design expansion']);
|
|
735
|
+
for (const falseTrigger of ['Requirement count', 'size', 'planning depth', 'assurance level', 'risk label', 'architectural layers']) {
|
|
736
|
+
assert.ok(router.includes(falseTrigger), `router must name inert trigger ${falseTrigger}`);
|
|
497
737
|
}
|
|
738
|
+
for (const boundary of ['ownership', 'dependency', 'transition', 'proof', 'parallel']) {
|
|
739
|
+
assert.ok(router.includes(boundary), `router must name typed topology ${boundary}`);
|
|
740
|
+
}
|
|
741
|
+
assert.ok(router.includes('unresolved material uncertainty'));
|
|
742
|
+
assert.ok(router.includes('external-current fact'));
|
|
743
|
+
assert.ok(router.includes('explicit user request'));
|
|
498
744
|
});
|
|
499
745
|
|
|
500
746
|
test('canonical verdict adapter handles completion and unfinished decisions', () => {
|
|
@@ -525,7 +771,7 @@ test('canonical verdict adapter handles completion and unfinished decisions', ()
|
|
|
525
771
|
|
|
526
772
|
test('completion decision requires canonical execution receipt and every workflow obligation', () => {
|
|
527
773
|
const receipt = canonicalReceipt();
|
|
528
|
-
const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
|
|
774
|
+
const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
|
|
529
775
|
const audit = {
|
|
530
776
|
schema_version: '1',
|
|
531
777
|
reviewer_session_id: 'review-session-1',
|
|
@@ -619,16 +865,228 @@ test('completion decision requires canonical execution receipt and every workflo
|
|
|
619
865
|
assert.ok(artifactBlocked.missingProof.some((item) => item.includes('artifact_hash')));
|
|
620
866
|
});
|
|
621
867
|
|
|
622
|
-
test('
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
868
|
+
test('auto authoring reaches readiness only after promotion, validation, grounding, and current review', () => {
|
|
869
|
+
const fixture = createCanonicalAuthoringFixture();
|
|
870
|
+
try {
|
|
871
|
+
let spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
872
|
+
assert.equal(spec.semantic_model, null);
|
|
873
|
+
spec.ready_for_implementation = true;
|
|
874
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
875
|
+
let validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
876
|
+
assert.ok(validation.errors.some((error) => error.includes('requires explicit promoted machine semantic authority')));
|
|
877
|
+
|
|
878
|
+
spec.ready_for_implementation = false;
|
|
879
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
880
|
+
promoteCanonicalModel(fixture);
|
|
881
|
+
spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
882
|
+
assert.ok(spec.semantic_model && spec.semantic_model.criteria.length === 2);
|
|
883
|
+
spec.ready_for_implementation = true;
|
|
884
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
885
|
+
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
886
|
+
assert.ok(validation.errors.some((error) => error.includes('readiness requires validated')));
|
|
887
|
+
assert.ok(validation.errors.some((error) => error.includes('requires completed semantic review')));
|
|
888
|
+
|
|
889
|
+
spec.ready_for_implementation = false;
|
|
890
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
891
|
+
spec = makeCanonicalReady(fixture);
|
|
892
|
+
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
893
|
+
assert.deepEqual(validation.errors, [], validation.errors.join('\n'));
|
|
894
|
+
assert.equal(GROUNDER.groundSpec({ specDir: fixture.specDir, root: fixture.root, spec }).errors.length, 0);
|
|
895
|
+
|
|
896
|
+
const specs = read(SPECS);
|
|
897
|
+
const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
|
|
898
|
+
assert.ok(executableBlocks.every((block) => !/hapo:?develop/i.test(block)), 'Specs must never execute Develop');
|
|
899
|
+
const handoff = markdownSection(specs, 'Platform acceptance and handoff');
|
|
900
|
+
assert.ok(handoff.includes('After readiness'));
|
|
901
|
+
assert.ok(handoff.includes('/hapo:develop <feature>'));
|
|
902
|
+
assert.ok(handoff.includes('$hapo-develop <feature>'));
|
|
903
|
+
assert.ok(handoff.includes('must stop in Specs'));
|
|
904
|
+
} finally {
|
|
905
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test('atomic readiness supports one-criterion Routine specs and binds semantic text', () => {
|
|
910
|
+
const fixture = createOrdinaryAuthoringFixture();
|
|
911
|
+
try {
|
|
912
|
+
const authored = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
913
|
+
authored.decisions = [
|
|
914
|
+
{ 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.' },
|
|
915
|
+
{ 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.' },
|
|
916
|
+
];
|
|
917
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(authored, null, 2)}\n`);
|
|
918
|
+
const result = READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() });
|
|
919
|
+
assert.equal(result.spec.ready_for_implementation, true);
|
|
920
|
+
assert.equal(result.spec.semantic_model.criteria.length, 1);
|
|
921
|
+
assert.match(result.spec.semantic_model.criteria[0].text, /delete the selected record/);
|
|
922
|
+
assert.deepEqual(VALIDATOR.validateSpec(fixture.specDir).errors, []);
|
|
923
|
+
|
|
924
|
+
const beforeDigest = result.semantic_digest;
|
|
925
|
+
const design = path.join(fixture.specDir, 'design.md');
|
|
926
|
+
const originalDesign = fs.readFileSync(design, 'utf8');
|
|
927
|
+
fs.writeFileSync(design, originalDesign.replace('deleted rather than archived', 'archived rather than deleted'));
|
|
928
|
+
const decisionProjection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
929
|
+
assert.notDeepEqual(decisionProjection.model.design_records, result.spec.semantic_model.design_records);
|
|
930
|
+
const decisionDigest = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: decisionProjection.model });
|
|
931
|
+
assert.equal(decisionDigest.errors.length, 0, decisionDigest.errors.join('\n'));
|
|
932
|
+
assert.notEqual(decisionDigest.digest, beforeDigest);
|
|
933
|
+
fs.writeFileSync(design, originalDesign);
|
|
934
|
+
|
|
935
|
+
const requirements = path.join(fixture.specDir, 'requirements.md');
|
|
936
|
+
fs.writeFileSync(requirements, fs.readFileSync(requirements, 'utf8').replace('delete the selected record', 'archive the selected record'));
|
|
937
|
+
const projection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
938
|
+
assert.notEqual(projection.model.criteria[0].text, result.spec.semantic_model.criteria[0].text);
|
|
939
|
+
const changed = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: projection.model });
|
|
940
|
+
assert.equal(changed.errors.length, 0, changed.errors.join('\n'));
|
|
941
|
+
assert.notEqual(changed.digest, beforeDigest);
|
|
942
|
+
assert.ok(VALIDATOR.validateSpec(fixture.specDir).errors.some((error) => /projection|stale/.test(error)));
|
|
943
|
+
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test('atomic readiness preserves exact bytes on review, grounding, decision, and Strict authority failures', () => {
|
|
947
|
+
const fixture = createOrdinaryAuthoringFixture();
|
|
948
|
+
try {
|
|
949
|
+
const original = fs.readFileSync(fixture.specPath);
|
|
950
|
+
for (const mutate of [
|
|
951
|
+
(review) => ({ ...review, reviewed_criteria: [] }),
|
|
952
|
+
(review) => ({ ...review, counterexamples: [] }),
|
|
953
|
+
]) {
|
|
954
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: mutate(ordinaryReview()) }), /readiness validation/);
|
|
955
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
956
|
+
}
|
|
957
|
+
fs.unlinkSync(path.join(fixture.root, 'src', 'entry.js'));
|
|
958
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /grounding|validation/);
|
|
959
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
960
|
+
fs.writeFileSync(path.join(fixture.root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
961
|
+
|
|
962
|
+
process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME = '1';
|
|
963
|
+
try {
|
|
964
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /injected finalization failure/);
|
|
965
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
966
|
+
} finally { delete process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME; }
|
|
967
|
+
|
|
968
|
+
const blocked = JSON.parse(original);
|
|
969
|
+
blocked.decisions = [{ id: 'Q1', classification: 'user_owned', statement: 'Choose permanent deletion or retention archive behavior.', status: 'unresolved', evidence: null }];
|
|
970
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
971
|
+
const blockedBytes = fs.readFileSync(fixture.specPath);
|
|
972
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /unresolved material user-owned decision/);
|
|
973
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(blockedBytes), true);
|
|
974
|
+
|
|
975
|
+
delete blocked.decisions;
|
|
976
|
+
blocked.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
977
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
978
|
+
const strictBytes = fs.readFileSync(fixture.specPath);
|
|
979
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
|
|
980
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(strictBytes), true);
|
|
981
|
+
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
test('Strict atomic readiness succeeds only after current allowlisted host observation', () => {
|
|
985
|
+
const fixture = createOrdinaryAuthoringFixture();
|
|
986
|
+
try {
|
|
987
|
+
const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
988
|
+
spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
989
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
990
|
+
const before = fs.readFileSync(fixture.specPath);
|
|
991
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
|
|
992
|
+
assert.equal(fs.readFileSync(fixture.specPath).equals(before), true);
|
|
993
|
+
|
|
994
|
+
const digest = VALIDATOR.computeSemanticDigest21(fixture.specDir, spec);
|
|
995
|
+
assert.equal(digest.errors.length, 0, digest.errors.join('\n'));
|
|
996
|
+
const authorityHome = path.join(fixture.root, 'authority-home');
|
|
997
|
+
const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({ feature_name: 'ordinary', spec_file: 'specs/ordinary/spec.json', semantic_digest: digest.digest, verdict: 'PASS' })}`;
|
|
998
|
+
const observed = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
|
|
999
|
+
cwd: fixture.root,
|
|
1000
|
+
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
1001
|
+
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 }),
|
|
1002
|
+
encoding: 'utf8',
|
|
1003
|
+
});
|
|
1004
|
+
assert.equal(observed.status, 0, observed.stderr);
|
|
1005
|
+
const reviewFile = path.join(fixture.root, 'review.json');
|
|
1006
|
+
fs.writeFileSync(reviewFile, `${JSON.stringify(ordinaryReview())}\n`);
|
|
1007
|
+
const finalized = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'), fixture.specDir, '--review-result', reviewFile], {
|
|
1008
|
+
cwd: fixture.root,
|
|
1009
|
+
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
1010
|
+
encoding: 'utf8',
|
|
1011
|
+
});
|
|
1012
|
+
assert.equal(finalized.status, 0, `${finalized.stdout}\n${finalized.stderr}`);
|
|
1013
|
+
assert.equal(JSON.parse(fs.readFileSync(fixture.specPath, 'utf8')).ready_for_implementation, true);
|
|
1014
|
+
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
test('done is a final-state request bound to current semantic model and digest', () => {
|
|
1018
|
+
const fixture = createCanonicalAuthoringFixture();
|
|
1019
|
+
try {
|
|
1020
|
+
promoteCanonicalModel(fixture);
|
|
1021
|
+
const spec = makeCanonicalReady(fixture);
|
|
1022
|
+
spec.status = 'done';
|
|
1023
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1024
|
+
const candidate = {
|
|
1025
|
+
featureDir: fixture.specDir,
|
|
1026
|
+
featureName: 'feature',
|
|
1027
|
+
specFile: fixture.specPath,
|
|
1028
|
+
spec,
|
|
1029
|
+
};
|
|
1030
|
+
const dependencies = () => ({
|
|
1031
|
+
validator: VALIDATOR,
|
|
1032
|
+
grounder: GROUNDER,
|
|
1033
|
+
semanticAuthority: SEMANTIC_AUTHORITY,
|
|
1034
|
+
});
|
|
1035
|
+
const current = FINAL_STATE.validateCanonicalFinalState({
|
|
1036
|
+
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1037
|
+
});
|
|
1038
|
+
assert.equal(current.ok, true, current.reason);
|
|
1039
|
+
assert.equal(current.semanticDigest, spec.validation.semantic_review.semantic_digest);
|
|
1040
|
+
|
|
1041
|
+
const failedReceipt = structuredClone(spec);
|
|
1042
|
+
failedReceipt.validation.semantic_review.verdict = 'FAIL';
|
|
1043
|
+
const receiptMutation = FINAL_STATE.validateCanonicalFinalState({
|
|
1044
|
+
policy: POLICY,
|
|
1045
|
+
projectRoot: fixture.root,
|
|
1046
|
+
candidate: { ...candidate, spec: failedReceipt },
|
|
1047
|
+
dependencies,
|
|
1048
|
+
});
|
|
1049
|
+
assert.equal(receiptMutation.ok, false);
|
|
1050
|
+
assert.match(receiptMutation.reason, /PASS CONTINUE/);
|
|
1051
|
+
|
|
1052
|
+
const mismatchedHistory = structuredClone(spec);
|
|
1053
|
+
const latest = mismatchedHistory.validation.semantic_review_history.entries[
|
|
1054
|
+
mismatchedHistory.validation.semantic_review_history.entries.length - 1
|
|
1055
|
+
];
|
|
1056
|
+
latest.review_receipt_digest = `sha256:${'0'.repeat(64)}`;
|
|
1057
|
+
const digestMismatch = FINAL_STATE.validateCanonicalFinalState({
|
|
1058
|
+
policy: POLICY,
|
|
1059
|
+
projectRoot: fixture.root,
|
|
1060
|
+
candidate: { ...candidate, spec: mismatchedHistory },
|
|
1061
|
+
dependencies,
|
|
1062
|
+
});
|
|
1063
|
+
assert.equal(digestMismatch.ok, false);
|
|
1064
|
+
assert.match(digestMismatch.reason, /review_receipt_digest|canonical receipt/);
|
|
1065
|
+
|
|
1066
|
+
const requirementsPath = path.join(fixture.specDir, 'requirements.md');
|
|
1067
|
+
const requirements = fs.readFileSync(requirementsPath, 'utf8');
|
|
1068
|
+
fs.writeFileSync(requirementsPath, requirements.replace('invalid input shall return rejected state', 'invalid input shall return a deterministic rejected state'));
|
|
1069
|
+
const staleDigest = FINAL_STATE.validateCanonicalFinalState({
|
|
1070
|
+
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1071
|
+
});
|
|
1072
|
+
assert.equal(staleDigest.ok, false);
|
|
1073
|
+
assert.match(staleDigest.reason, /stale|differs from Markdown projection/i);
|
|
1074
|
+
|
|
1075
|
+
fs.writeFileSync(requirementsPath, requirements);
|
|
1076
|
+
const staleModel = structuredClone(spec);
|
|
1077
|
+
staleModel.semantic_model.criteria[0].owner = 'A-D-02';
|
|
1078
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(staleModel, null, 2)}\n`);
|
|
1079
|
+
const invalidModel = FINAL_STATE.validateCanonicalFinalState({
|
|
1080
|
+
policy: POLICY,
|
|
1081
|
+
projectRoot: fixture.root,
|
|
1082
|
+
candidate: { ...candidate, spec: staleModel },
|
|
1083
|
+
dependencies,
|
|
1084
|
+
});
|
|
1085
|
+
assert.equal(invalidModel.ok, false);
|
|
1086
|
+
assert.match(invalidModel.reason, /semantic projection|semantic_model|validation failed/i);
|
|
1087
|
+
} finally {
|
|
1088
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
1089
|
+
}
|
|
632
1090
|
});
|
|
633
1091
|
|
|
634
1092
|
test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
|
|
@@ -665,22 +1123,19 @@ test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
|
|
|
665
1123
|
|
|
666
1124
|
test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
|
|
667
1125
|
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'));
|
|
1126
|
+
const specGate = installClaudeRuntimeClosure(root);
|
|
1127
|
+
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'spec-final-state.cjs')), true);
|
|
1128
|
+
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'validate-spec-output.cjs')), true);
|
|
675
1129
|
fs.mkdirSync(path.join(root, 'specs', 'demo', 'tasks'), { recursive: true });
|
|
676
1130
|
fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
|
|
677
1131
|
status: 'in_progress',
|
|
1132
|
+
current_phase: 'closeout',
|
|
678
1133
|
feature_name: 'demo',
|
|
679
1134
|
task_registry: { 'tasks/task.md': { status: 'done', receipt: 'FLASH_UNVERIFIED' } },
|
|
680
1135
|
}));
|
|
681
1136
|
initFixtureGit(root);
|
|
682
1137
|
try {
|
|
683
|
-
const result = spawnSync(process.execPath, [
|
|
1138
|
+
const result = spawnSync(process.execPath, [specGate], {
|
|
684
1139
|
cwd: root,
|
|
685
1140
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
686
1141
|
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
@@ -698,24 +1153,21 @@ test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
|
|
|
698
1153
|
test('first-run cache bypass is blocked - canonical receipt required even without cache', () => {
|
|
699
1154
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-first-run-'));
|
|
700
1155
|
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'));
|
|
1156
|
+
const specGate = installClaudeRuntimeClosure(root);
|
|
707
1157
|
const specDir = path.join(root, 'specs', 'demo', 'tasks');
|
|
708
1158
|
fs.mkdirSync(specDir, { recursive: true });
|
|
709
1159
|
// Done task without canonical receipt (missing Command, provenance)
|
|
710
1160
|
fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
|
|
711
|
-
status: '
|
|
1161
|
+
status: 'done',
|
|
1162
|
+
current_phase: 'closeout',
|
|
712
1163
|
feature_name: 'demo',
|
|
713
|
-
task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
|
|
1164
|
+
task_registry: { 'tasks/task-R1-01-one.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
|
|
714
1165
|
}));
|
|
715
|
-
fs.writeFileSync(path.join(specDir, 'task.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\n```\nnpm test\n```\n');
|
|
1166
|
+
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
1167
|
initFixtureGit(root);
|
|
717
1168
|
try {
|
|
718
|
-
|
|
1169
|
+
assert.equal(fs.existsSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')), false);
|
|
1170
|
+
const result = spawnSync(process.execPath, [specGate], {
|
|
719
1171
|
cwd: root,
|
|
720
1172
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
721
1173
|
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
@@ -724,15 +1176,25 @@ test('first-run cache bypass is blocked - canonical receipt required even withou
|
|
|
724
1176
|
assert.equal(result.status, 0);
|
|
725
1177
|
const payload = JSON.parse(result.stdout);
|
|
726
1178
|
assert.equal(payload.decision, 'block', 'first-run without canonical receipt must block');
|
|
727
|
-
assert.match(payload.reason, /tasks\/task\.md/);
|
|
1179
|
+
assert.match(payload.reason, /tasks\/task-R1-01-one\.md/);
|
|
1180
|
+
assert.match(payload.reason, /receipt|command|provenance/i);
|
|
728
1181
|
// Also test that with canonical receipt it passes
|
|
729
|
-
fs.writeFileSync(path.join(specDir, 'task.md'),
|
|
1182
|
+
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');
|
|
1183
|
+
fs.mkdirSync(path.join(root, 'specs', 'demo', 'receipts'), { recursive: true });
|
|
1184
|
+
fs.writeFileSync(
|
|
1185
|
+
path.join(root, 'specs', 'demo', 'receipts', 'task-R1-01-one.md'),
|
|
1186
|
+
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'),
|
|
1187
|
+
);
|
|
1188
|
+
fs.writeFileSync(
|
|
1189
|
+
path.join(root, 'specs', 'demo', 'feature-receipt.md'),
|
|
1190
|
+
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'),
|
|
1191
|
+
);
|
|
730
1192
|
// Clear cache to simulate fresh first-run with valid receipt
|
|
731
1193
|
try { fs.unlinkSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')); } catch {}
|
|
732
1194
|
try { fs.unlinkSync(path.join(__dirname, '..', '.logs', 'spec-gate-last.json')); } catch {}
|
|
733
1195
|
// Need to clear the cache that the hook uses (under claude hooks .logs) - we already cleared above
|
|
734
1196
|
// Re-run with valid receipt - should not block
|
|
735
|
-
const result2 = spawnSync(process.execPath, [
|
|
1197
|
+
const result2 = spawnSync(process.execPath, [specGate], {
|
|
736
1198
|
cwd: root,
|
|
737
1199
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
738
1200
|
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
@@ -986,7 +1448,7 @@ test('runtime provenance derives exact Git evidence, CLI context, and stale/forg
|
|
|
986
1448
|
}
|
|
987
1449
|
});
|
|
988
1450
|
|
|
989
|
-
test('lane traces - Direct, Standard,
|
|
1451
|
+
test('lane traces - Direct, Standard, explicit Strict classification, override, state mutation and completion', () => {
|
|
990
1452
|
// Direct: isolated reversible low-risk
|
|
991
1453
|
const direct = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
|
|
992
1454
|
assert.equal(direct.lane, 'Direct');
|
|
@@ -997,29 +1459,30 @@ test('lane traces - Direct, Standard, Critical classification, override, state m
|
|
|
997
1459
|
// Standard: default
|
|
998
1460
|
const standard = POLICY.classifyLane({ title: 'add pagination to list', taskCount: 1 });
|
|
999
1461
|
assert.equal(standard.lane, 'Standard');
|
|
1000
|
-
assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['
|
|
1462
|
+
assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['needsExecutionProof']);
|
|
1001
1463
|
assert.equal(POLICY.lanePolicy(standard).shipPoint, 'feature');
|
|
1002
|
-
// Critical
|
|
1003
|
-
const
|
|
1464
|
+
// Risk alone is Elevated; Critical requires an explicit Strict assurance choice.
|
|
1465
|
+
const elevated = POLICY.classifyLane({ riskSignals: { auth: true }, taskCount: 1 });
|
|
1466
|
+
assert.equal(elevated.lane, 'Standard');
|
|
1467
|
+
assert.deepEqual(POLICY.lanePolicy(elevated).proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
1468
|
+
const critical = POLICY.classifyLane({ riskSignals: { auth: true }, assurance_level: 'Strict', taskCount: 1 });
|
|
1004
1469
|
assert.equal(critical.lane, 'Critical');
|
|
1005
1470
|
assert.ok(critical.risks.includes('auth'));
|
|
1006
|
-
assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit'
|
|
1471
|
+
assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
1007
1472
|
// Override: Direct requesting Critical is allowed (upgrade) without extra auth
|
|
1008
1473
|
const upgrade = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1, override: 'Critical' });
|
|
1009
1474
|
assert.equal(upgrade.lane, 'Critical');
|
|
1010
1475
|
assert.equal(upgrade.automaticLane, 'Direct');
|
|
1011
1476
|
// 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/);
|
|
1477
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /assurance_level downgrade.*blocked/i);
|
|
1478
|
+
// Caller-owned booleans cannot weaken the classified minimum.
|
|
1479
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true }), /assurance_level downgrade.*blocked/i);
|
|
1480
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', user_approved: true }), /assurance_level downgrade.*blocked/i);
|
|
1018
1481
|
// State mutation: approvalState
|
|
1019
1482
|
const pendingApproval = POLICY.approvalState({ generated: true, agent_validated: false, user_approved: false });
|
|
1020
1483
|
assert.equal(pendingApproval.ready, false);
|
|
1021
1484
|
const agentValidated = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false });
|
|
1022
|
-
assert.equal(agentValidated.ready,
|
|
1485
|
+
assert.equal(agentValidated.ready, true, 'generated plus agent validation is technical readiness');
|
|
1023
1486
|
const approved = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true });
|
|
1024
1487
|
assert.equal(approved.ready, true);
|
|
1025
1488
|
// Completion: flash work remains in_progress and does not unblock
|
|
@@ -1946,24 +2409,29 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
|
|
|
1946
2409
|
}
|
|
1947
2410
|
});
|
|
1948
2411
|
|
|
1949
|
-
test('P0
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
assert.
|
|
1957
|
-
assert.equal(POLICY.isUserAuthorizedForDowngrade
|
|
2412
|
+
test('P0 caller-owned downgrade flags have no canonical capability', () => {
|
|
2413
|
+
for (const callerClaim of [{ user_approved: true }, { userApproved: true }, { userAuthorized: true }]) {
|
|
2414
|
+
assert.throws(
|
|
2415
|
+
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct', ...callerClaim }),
|
|
2416
|
+
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
assert.throws(() => POLICY.classifyLane({ riskSignals: { auth: true }, override: 'Standard', confirmDowngrade: true }), /assurance_level downgrade.*blocked/i);
|
|
2420
|
+
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
1958
2421
|
});
|
|
1959
2422
|
|
|
1960
|
-
test('P0
|
|
1961
|
-
|
|
1962
|
-
assert.equal(POLICY.isValidOverrideReceipt
|
|
1963
|
-
assert.
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2423
|
+
test('P0 canonical policy exposes no caller-owned downgrade authority', () => {
|
|
2424
|
+
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
2425
|
+
assert.equal(POLICY.isValidOverrideReceipt, undefined);
|
|
2426
|
+
assert.equal(POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS.includes('override_' + 'receipt'), false);
|
|
2427
|
+
assert.throws(
|
|
2428
|
+
() => POLICY.classifyLane({
|
|
2429
|
+
riskSignals: { privacy: true },
|
|
2430
|
+
override: 'Direct',
|
|
2431
|
+
overrideReceipt: { verifiedByRuntime: true },
|
|
2432
|
+
}),
|
|
2433
|
+
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2434
|
+
);
|
|
1967
2435
|
});
|
|
1968
2436
|
|
|
1969
2437
|
test('P0 active spec deterministic: multiple active ambiguity and explicit target/path containment', () => {
|
|
@@ -2056,7 +2524,7 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
|
|
|
2056
2524
|
const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
|
|
2057
2525
|
const forged = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct', riskSignals: { auth: true } }), '--json'], { encoding: 'utf8' });
|
|
2058
2526
|
assert.equal(forged.status, 2, 'forged Direct with auth should be blocked via lanePolicy');
|
|
2059
|
-
assert.match(forged.stderr, /
|
|
2527
|
+
assert.match(forged.stderr, /downgrade.*not permitted/i);
|
|
2060
2528
|
const forged2 = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct' }), '--json'], { encoding: 'utf8' });
|
|
2061
2529
|
assert.equal(forged2.status, 2, 'plain Direct without justification should be blocked');
|
|
2062
2530
|
|
|
@@ -2067,7 +2535,7 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
|
|
|
2067
2535
|
assert.throws(() => { 'use strict'; trusted.lane = 'Direct'; }, /read only|Cannot assign|TypeError/);
|
|
2068
2536
|
const paymentTrusted = POLICY.classifyLane({ riskSignals: { payment: true } });
|
|
2069
2537
|
const forgedCopy = { ...paymentTrusted, lane: 'Direct' };
|
|
2070
|
-
assert.
|
|
2538
|
+
assert.equal(POLICY.lanePolicy(forgedCopy).lane, 'Standard', 'compatibility lane is derived from authoritative axes');
|
|
2071
2539
|
});
|
|
2072
2540
|
|
|
2073
2541
|
test('P0 regression: symlink spec/task containment and malformed spec handling', () => {
|