@haposoft/cafekit 0.18.0 → 0.18.1
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/README.md +1 -1
- package/bin/__tests__/codex-hooks.test.js +169 -236
- package/bin/__tests__/codex-native.test.js +54 -114
- package/bin/__tests__/develop-contract.test.js +101 -1361
- package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/codex-runtime.js +6 -3
- package/bin/phases/copy-payload.js +13 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +170 -330
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +26 -0
- package/src/claude/hooks/spec-gate.cjs +8 -63
- package/src/claude/hooks/spec-state.cjs +47 -38
- package/src/claude/migration-manifest.json +12 -7
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- package/src/codex/hooks/lib/spec-utils.cjs +6 -45
- package/src/codex/hooks/spec-gate.cjs +11 -68
- package/src/codex/hooks/spec-state.cjs +37 -41
- package/src/common/AGENTS.md +4 -10
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
|
@@ -14,15 +14,9 @@ const POLICY = require(POLICY_PATH);
|
|
|
14
14
|
const PROVENANCE_HELPER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/provenance.cjs'));
|
|
15
15
|
const RESOLVER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
|
|
16
16
|
const RECEIPTS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-receipt.cjs'));
|
|
17
|
-
const FINAL_STATE = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-final-state.cjs'));
|
|
18
|
-
const VALIDATOR = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-spec-output.cjs'));
|
|
19
|
-
const GROUNDER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-ground.cjs'));
|
|
20
|
-
const READINESS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'));
|
|
21
|
-
const AUTHORING_VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
|
|
22
17
|
const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
|
|
23
18
|
const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
|
|
24
19
|
const SPECS = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md');
|
|
25
|
-
const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
|
|
26
20
|
const GATE = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/quality-gate.md');
|
|
27
21
|
const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
|
|
28
22
|
const TEST_SOURCE_ROOT = path.dirname(TEST_SKILL);
|
|
@@ -64,7 +58,6 @@ after(() => fs.rmSync(RUNTIME_ROOT, { recursive: true, force: true }));
|
|
|
64
58
|
const EXPECTED_BASE = RUNTIME_CONTEXT.base;
|
|
65
59
|
const EXPECTED_HEAD = RUNTIME_CONTEXT.head;
|
|
66
60
|
const EXPECTED_PROVENANCE = { base: EXPECTED_BASE, head: EXPECTED_HEAD };
|
|
67
|
-
const RECEIPT_BINDING = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
|
|
68
61
|
|
|
69
62
|
function initFixtureGit(root) {
|
|
70
63
|
for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['add', '-A'], ['commit', '-qm', 'fixture']]) {
|
|
@@ -159,182 +152,10 @@ function assertVocabularyIsLegacyOnly(filePath, regions) {
|
|
|
159
152
|
);
|
|
160
153
|
}
|
|
161
154
|
|
|
162
|
-
function runNode(root, script, args) {
|
|
163
|
-
return spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function commandOutput(result) {
|
|
167
|
-
return `${result.stdout}\n${result.stderr}`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function createCanonicalAuthoringFixture() {
|
|
171
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-develop-contract-'));
|
|
172
|
-
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
173
|
-
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
174
|
-
fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
|
|
175
|
-
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { enabled: true };\n');
|
|
176
|
-
fs.writeFileSync(path.join(root, 'test', 'service.test.js'), 'require("node:test")("service", () => {});\n');
|
|
177
|
-
const scaffold = runNode(root, SCAFFOLD, ['feature']);
|
|
178
|
-
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
179
|
-
const specDir = path.join(root, 'specs', 'feature');
|
|
180
|
-
const specPath = path.join(specDir, 'spec.json');
|
|
181
|
-
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
182
|
-
|
|
183
|
-
## Requirements
|
|
184
|
-
### Requirement 1: Observable service behavior
|
|
185
|
-
- **R1.1**: When valid input arrives, the service shall return enabled state; invalid input shall return rejected state.
|
|
186
|
-
- **R1.2**: The verifier shall produce separate observable proof for enabled and rejected states.
|
|
187
|
-
`);
|
|
188
|
-
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
189
|
-
|
|
190
|
-
## Architecture
|
|
191
|
-
The entrypoint exposes the bounded service behavior and a separate proof artifact.
|
|
192
|
-
|
|
193
|
-
## Typed Anchors
|
|
194
|
-
| ID | Type | Target | Role | Access | Action |
|
|
195
|
-
|---|---|---|---|---|---|
|
|
196
|
-
| A-D-01 | file | \`src/entry.js\` | service boundary | read | read |
|
|
197
|
-
| A-D-02 | artifact | \`artifacts/design-proof.json\` | proof evidence | write | create |
|
|
198
|
-
|
|
199
|
-
## Canonical Contracts & Invariants
|
|
200
|
-
### D1 — Dispatch decision
|
|
201
|
-
The entrypoint dispatches exactly once.
|
|
202
|
-
### I1 — Invalid-state invariant
|
|
203
|
-
Invalid input never returns enabled state.
|
|
204
|
-
### C1 — Result contract
|
|
205
|
-
The result contains one enabled boolean.
|
|
206
|
-
|
|
207
|
-
## Verification Definitions
|
|
208
|
-
- **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.
|
|
209
|
-
`);
|
|
210
|
-
return { root, specDir, specPath };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function createOrdinaryAuthoringFixture() {
|
|
214
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-ordinary-readiness-'));
|
|
215
|
-
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
216
|
-
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
217
|
-
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
218
|
-
fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), 'require("node:test")("entry", () => {});\n');
|
|
219
|
-
const scaffold = runNode(root, SCAFFOLD, ['ordinary']);
|
|
220
|
-
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
221
|
-
const specDir = path.join(root, 'specs', 'ordinary');
|
|
222
|
-
const specPath = path.join(specDir, 'spec.json');
|
|
223
|
-
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
224
|
-
|
|
225
|
-
## Requirements
|
|
226
|
-
### Requirement 1: Delete behavior
|
|
227
|
-
- **R1.1**: When deletion is requested, the service shall delete the selected record and report deleted state.
|
|
228
|
-
`);
|
|
229
|
-
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
230
|
-
|
|
231
|
-
## Typed Anchors
|
|
232
|
-
| ID | Type | Target | Role | Access | Action |
|
|
233
|
-
|---|---|---|---|---|---|
|
|
234
|
-
| A-D-01 | file | \`src/entry.js\` | behavior owner | read | read |
|
|
235
|
-
|
|
236
|
-
## Canonical Contracts & Invariants
|
|
237
|
-
### D1 — Delete decision
|
|
238
|
-
The selected record is deleted rather than archived.
|
|
239
|
-
### I1 — State invariant
|
|
240
|
-
Deleted state is reported only after deletion succeeds.
|
|
241
|
-
### C1 — Result contract
|
|
242
|
-
The result contains the literal deleted state.
|
|
243
|
-
|
|
244
|
-
## Verification Definitions
|
|
245
|
-
- **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.
|
|
246
|
-
`);
|
|
247
|
-
const semantic = runNode(root, SCAFFOLD, ['ordinary', '--sync-semantic-model']);
|
|
248
|
-
assert.equal(semantic.status, 0, commandOutput(semantic));
|
|
249
|
-
const authoring = runNode(root, AUTHORING_VALIDATOR, [specDir]);
|
|
250
|
-
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
251
|
-
initFixtureGit(root);
|
|
252
|
-
return { root, specDir, specPath };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function ordinaryReview() {
|
|
256
|
-
return canonicalPassReview(['R1.1'], [{
|
|
257
|
-
criterion: 'R1.1', case_kind: 'failure',
|
|
258
|
-
scenario: 'Deletion targets a record that does not exist in the current store.',
|
|
259
|
-
expected: 'The service returns not-found and never reports the record as deleted.',
|
|
260
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
261
|
-
}]);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function canonicalPassReview(reviewedCriteria, counterexamples) {
|
|
265
|
-
return {
|
|
266
|
-
verdict: 'PASS',
|
|
267
|
-
findings: [],
|
|
268
|
-
unresolved_decisions: [],
|
|
269
|
-
graph_coverage: [
|
|
270
|
-
'criterion_local', 'cross_criterion', 'runtime_path',
|
|
271
|
-
'assumption_provenance', 'compatibility_migration',
|
|
272
|
-
].map((surface) => ({
|
|
273
|
-
surface, covered: true,
|
|
274
|
-
notes: 'The review covers this semantic surface against the canonical model.',
|
|
275
|
-
})),
|
|
276
|
-
reviewed_criteria: reviewedCriteria,
|
|
277
|
-
counterexamples,
|
|
278
|
-
reviewer_evidence: null,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function promoteCanonicalModel(fixture) {
|
|
283
|
-
const result = runNode(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
|
|
284
|
-
assert.equal(result.status, 0, commandOutput(result));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function makeCanonicalReady(fixture) {
|
|
288
|
-
const authoring = runNode(fixture.root, AUTHORING_VALIDATOR, [fixture.specDir]);
|
|
289
|
-
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
290
|
-
const result = READINESS.finalizeReadiness({
|
|
291
|
-
specDir: fixture.specDir,
|
|
292
|
-
projectRoot: fixture.root,
|
|
293
|
-
reviewResult: canonicalPassReview(['R1.1', 'R1.2'], [{
|
|
294
|
-
criterion: 'R1.1', case_kind: 'failure',
|
|
295
|
-
scenario: 'The service receives invalid input without its required discriminator.',
|
|
296
|
-
expected: 'The service returns rejected state and never emits enabled true.',
|
|
297
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
298
|
-
}, {
|
|
299
|
-
criterion: 'R1.2', case_kind: 'adversarial',
|
|
300
|
-
scenario: 'The service reports enabled state without producing separate proof evidence.',
|
|
301
|
-
expected: 'Verification fails because the independent proof artifact is absent.',
|
|
302
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
303
|
-
}]),
|
|
304
|
-
});
|
|
305
|
-
return result.spec;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function bindFixtureReceipt(root, value, feature = 'demo', session = 'test') {
|
|
309
|
-
const context = PROVENANCE_HELPER.deriveRuntimeContext({
|
|
310
|
-
projectRoot: root,
|
|
311
|
-
specsRoot: path.join(root, 'specs'),
|
|
312
|
-
specFile: path.join(root, 'specs', feature, 'spec.json'),
|
|
313
|
-
featureName: feature,
|
|
314
|
-
runtimeSession: session,
|
|
315
|
-
});
|
|
316
|
-
return value.replaceAll('0123456789abcdef0123456789abcdef01234567', context.base)
|
|
317
|
-
.replaceAll('89abcdef0123456789abcdef0123456789abcdef', context.head);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
155
|
function canonicalReceipt(command = 'pnpm test', extra = '') {
|
|
321
156
|
return `Verification: PASS\nCommand: ${command}\nExit: 0\nBase: ${EXPECTED_BASE}\nHead: ${EXPECTED_HEAD}\n${extra}`;
|
|
322
157
|
}
|
|
323
158
|
|
|
324
|
-
function canonicalFlashTask(overrides = {}) {
|
|
325
|
-
return {
|
|
326
|
-
status: 'in_progress',
|
|
327
|
-
receipt: 'FLASH_UNVERIFIED',
|
|
328
|
-
blocker: 'awaiting /cf:test <feature>',
|
|
329
|
-
dependencyBlocked: true,
|
|
330
|
-
unblocks: false,
|
|
331
|
-
feature_name: 'demo',
|
|
332
|
-
runtime_context: RUNTIME_CONTEXT,
|
|
333
|
-
expected_provenance: { ...EXPECTED_PROVENANCE },
|
|
334
|
-
...overrides,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
159
|
function read(filePath) {
|
|
339
160
|
return fs.readFileSync(filePath, 'utf8');
|
|
340
161
|
}
|
|
@@ -377,380 +198,17 @@ test('develop flags retain their process-v3 contracts', () => {
|
|
|
377
198
|
assert.match(develop, /FLASH_UNVERIFIED[\s\S]*do not unblock dependents[\s\S]*sync-finalize path/i);
|
|
378
199
|
});
|
|
379
200
|
|
|
380
|
-
test('
|
|
381
|
-
const light = POLICY.delegationPlan({ tier: 'Light', mode: 'full-spec', taskCount: 2 });
|
|
382
|
-
const standard = POLICY.delegationPlan({ tier: 'Standard', mode: 'specific-task' });
|
|
383
|
-
const deep = POLICY.delegationPlan({ tier: 'Deep', mode: 'full-spec', taskCount: 2 });
|
|
384
|
-
assert.deepEqual(light.proof_obligations, ['needsExecutionProof']);
|
|
385
|
-
assert.deepEqual(standard.proof_obligations, ['needsExecutionProof']);
|
|
386
|
-
assert.deepEqual(deep.proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
387
|
-
for (const plan of [light, standard, deep]) {
|
|
388
|
-
assert.equal(Object.hasOwn(plan, 'delegated'), false);
|
|
389
|
-
assert.doesNotMatch(JSON.stringify(plan), /inspector|implementer|test-runner|code-auditor/);
|
|
390
|
-
}
|
|
201
|
+
test('review and sync skills keep the verdict vocabulary and the sync-finalize path', () => {
|
|
391
202
|
assert.doesNotMatch(read(GATE), /spec-review|quality-review/);
|
|
392
203
|
assert.doesNotMatch(read(DEVELOP), /spec-review|quality-review/);
|
|
204
|
+
assert.match(read(GATE), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
205
|
+
assert.match(read(CODE_REVIEW_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
206
|
+
assert.match(read(TEST_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
207
|
+
assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/i);
|
|
208
|
+
assert.match(read(SYNC_SKILL), /sync-finalize/);
|
|
393
209
|
});
|
|
394
210
|
|
|
395
|
-
test('
|
|
396
|
-
const result = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
|
|
397
|
-
assert.equal(result.lane, 'Direct');
|
|
398
|
-
assert.equal(result.automaticLane, 'Direct');
|
|
399
|
-
assert.deepEqual(result.risks, []);
|
|
400
|
-
const policy = POLICY.lanePolicy(result);
|
|
401
|
-
assert.deepEqual(policy.proof_obligations, ['needsExecutionProof']);
|
|
402
|
-
assert.equal(policy.artifact_profile, 'targeted');
|
|
403
|
-
assert.equal(policy.requiresSpec, false);
|
|
404
|
-
assert.equal(policy.requiresState, false);
|
|
405
|
-
assert.equal(policy.proof_obligations.includes('needsDurableTaskState'), false);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test('lane classifier raises named risks to Elevated without inventing independent-audit authority', () => {
|
|
409
|
-
const result = POLICY.classifyLane({
|
|
410
|
-
reversible: true,
|
|
411
|
-
riskSignals: { auth: true, migration: true, publicContract: true },
|
|
412
|
-
});
|
|
413
|
-
assert.equal(result.lane, 'Standard');
|
|
414
|
-
assert.equal(result.assuranceLevel, 'Elevated');
|
|
415
|
-
assert.equal(result.automaticAssuranceLevel, 'Elevated');
|
|
416
|
-
assert.deepEqual(result.risks, ['auth', 'migration', 'publicContract']);
|
|
417
|
-
assert.deepEqual(POLICY.lanePolicy(result).proof_obligations, [
|
|
418
|
-
'needsInspection', 'needsExecutionProof',
|
|
419
|
-
]);
|
|
420
|
-
|
|
421
|
-
const strict = POLICY.classifyLane({
|
|
422
|
-
riskSignals: { auth: true },
|
|
423
|
-
assurance_level: 'Strict',
|
|
424
|
-
});
|
|
425
|
-
assert.equal(strict.lane, 'Critical');
|
|
426
|
-
assert.equal(strict.automaticLane, 'Standard');
|
|
427
|
-
assert.equal(strict.assuranceLevel, 'Strict');
|
|
428
|
-
assert.equal(strict.automaticAssuranceLevel, 'Elevated');
|
|
429
|
-
assert.ok(POLICY.lanePolicy(strict).proof_obligations.includes('needsIndependentAudit'));
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
test('P1 persists the minimal v2.1 policy and derives compatibility views', () => {
|
|
433
|
-
const initial = POLICY.persistWorkflowPolicySnapshot(
|
|
434
|
-
{ feature_name: 'bounded' },
|
|
435
|
-
{ riskSignals: {} },
|
|
436
|
-
);
|
|
437
|
-
const snapshot = initial.workflow_policy;
|
|
438
|
-
assert.deepEqual(Object.keys(snapshot).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
439
|
-
assert.equal(snapshot.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
440
|
-
assert.equal(snapshot.planning_depth, 'Compact');
|
|
441
|
-
assert.equal(snapshot.assurance_level, 'Routine');
|
|
442
|
-
assert.deepEqual(snapshot.classified_minimum, {
|
|
443
|
-
planning_depth: 'Compact',
|
|
444
|
-
assurance_level: 'Routine',
|
|
445
|
-
});
|
|
446
|
-
for (const derivedField of ['lane', 'automatic_lane', 'artifact_profile', 'proof_obligations', 'actor_needs']) {
|
|
447
|
-
assert.equal(Object.hasOwn(snapshot, derivedField), false, `${derivedField} must not be persisted`);
|
|
448
|
-
}
|
|
449
|
-
const view = POLICY.readWorkflowPolicySnapshot(initial);
|
|
450
|
-
assert.equal(view.lane, 'Standard');
|
|
451
|
-
assert.equal(view.automatic_lane, 'Standard');
|
|
452
|
-
assert.equal(view.artifact_profile, 'bounded');
|
|
453
|
-
assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
|
|
454
|
-
assert.equal(Object.hasOwn(initial, 'override_' + 'receipt'), false);
|
|
455
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(snapshot).valid, true);
|
|
456
|
-
|
|
457
|
-
const persistedAgain = POLICY.persistWorkflowPolicySnapshot(initial, {
|
|
458
|
-
riskSignals: { auth: true },
|
|
459
|
-
override: 'Direct',
|
|
460
|
-
});
|
|
461
|
-
assert.deepEqual(persistedAgain.workflow_policy, snapshot, 'persist-once must not reclassify an existing snapshot');
|
|
462
|
-
|
|
463
|
-
const malformed = { ...snapshot, lane: 'Critical' };
|
|
464
|
-
const validation = POLICY.validateWorkflowPolicySnapshot(malformed);
|
|
465
|
-
assert.equal(validation.valid, false);
|
|
466
|
-
assert.match(validation.errors.join('; '), /v2\.1 fields must be exactly/);
|
|
467
|
-
assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: malformed }), /Invalid workflow policy snapshot/);
|
|
468
|
-
|
|
469
|
-
const malformedShape = { ...snapshot, classified_minimum: { planning_depth: 'Compact' } };
|
|
470
|
-
const shapeValidation = POLICY.validateWorkflowPolicySnapshot(malformedShape);
|
|
471
|
-
assert.equal(shapeValidation.valid, false);
|
|
472
|
-
assert.match(shapeValidation.errors.join('; '), /classified_minimum must contain exactly/);
|
|
473
|
-
|
|
474
|
-
const malformedRisks = { ...snapshot, risks: { auth: true } };
|
|
475
|
-
const risksValidation = POLICY.validateWorkflowPolicySnapshot(malformedRisks);
|
|
476
|
-
assert.equal(risksValidation.valid, false);
|
|
477
|
-
assert.match(risksValidation.errors.join('; '), /risks must be an array/);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
test('P1 workflow-policy validation is semantic and explicit workflow_policy values never reclassify', () => {
|
|
481
|
-
const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
482
|
-
const standardUnknown = POLICY.escalateWorkflowPolicy(direct, { risks: ['unclassified-risk'] });
|
|
483
|
-
const elevatedAuth = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
|
|
484
|
-
const escalatedElevated = POLICY.escalateWorkflowPolicy(direct, { risks: ['auth'] });
|
|
485
|
-
|
|
486
|
-
for (const legitimate of [direct, standardUnknown, elevatedAuth, escalatedElevated]) {
|
|
487
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(legitimate).valid, true);
|
|
488
|
-
}
|
|
489
|
-
assert.equal(escalatedElevated.lane, 'Standard');
|
|
490
|
-
assert.equal(escalatedElevated.automatic_lane, 'Standard');
|
|
491
|
-
assert.equal(escalatedElevated.planning_depth, 'None');
|
|
492
|
-
assert.equal(escalatedElevated.assurance_level, 'Elevated');
|
|
493
|
-
|
|
494
|
-
const forgedDirectAuth = { ...direct, risks: ['auth'] };
|
|
495
|
-
const forgedDirectUnknown = { ...direct, risks: ['unclassified-risk'] };
|
|
496
|
-
const forgedStandardAuth = { ...standardUnknown, risks: ['auth'] };
|
|
497
|
-
for (const forged of [forgedDirectAuth, forgedDirectUnknown]) {
|
|
498
|
-
const result = POLICY.validateWorkflowPolicySnapshot(forged);
|
|
499
|
-
assert.equal(result.valid, false);
|
|
500
|
-
assert.match(result.errors.join('; '), /at least (Elevated|Strict)/);
|
|
501
|
-
}
|
|
502
|
-
assert.equal(
|
|
503
|
-
POLICY.validateWorkflowPolicySnapshot(forgedStandardAuth).valid,
|
|
504
|
-
true,
|
|
505
|
-
'changing one normalized risk to another does not invent a higher assurance minimum',
|
|
506
|
-
);
|
|
507
|
-
for (const riskAlias of ['Auth', 'authentication', 'public_contract']) {
|
|
508
|
-
const escalated = POLICY.escalateWorkflowPolicy(direct, { risks: [riskAlias] });
|
|
509
|
-
assert.equal(escalated.lane, 'Standard', `${riskAlias} must classify as Elevated`);
|
|
510
|
-
assert.equal(escalated.assurance_level, 'Elevated');
|
|
511
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot({ ...direct, risks: [riskAlias] }).valid, false);
|
|
512
|
-
}
|
|
513
|
-
assert.throws(() => POLICY.assertWorkflowPolicySnapshot(forgedDirectAuth), /Invalid workflow policy snapshot/);
|
|
514
|
-
assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
|
|
515
|
-
assert.throws(() => POLICY.lanePolicy({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
|
|
516
|
-
|
|
517
|
-
for (const explicitValue of [null, undefined]) {
|
|
518
|
-
const state = { workflow_policy: explicitValue, reversible: true, lowRisk: true, isolated: true };
|
|
519
|
-
assert.throws(() => POLICY.workflowPolicySnapshot(state), /Invalid workflow policy snapshot/);
|
|
520
|
-
assert.throws(() => POLICY.escalateWorkflowPolicy(state, {}), /Invalid workflow policy snapshot/);
|
|
521
|
-
assert.throws(() => POLICY.lanePolicy(state), /Invalid workflow policy snapshot/);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const forgedLanePolicyCli = spawnSync(process.execPath, [
|
|
525
|
-
POLICY_PATH,
|
|
526
|
-
'--lane-policy',
|
|
527
|
-
'--task-json',
|
|
528
|
-
JSON.stringify({ workflow_policy: forgedDirectAuth }),
|
|
529
|
-
'--json',
|
|
530
|
-
], { encoding: 'utf8' });
|
|
531
|
-
assert.equal(forgedLanePolicyCli.status, 2, `${forgedLanePolicyCli.stdout}\n${forgedLanePolicyCli.stderr}`);
|
|
532
|
-
assert.match(forgedLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
|
|
533
|
-
|
|
534
|
-
const nullLanePolicyCli = spawnSync(process.execPath, [
|
|
535
|
-
POLICY_PATH,
|
|
536
|
-
'--lane-policy',
|
|
537
|
-
'--task-json',
|
|
538
|
-
JSON.stringify({ workflow_policy: null, reversible: true, lowRisk: true, isolated: true }),
|
|
539
|
-
'--json',
|
|
540
|
-
], { encoding: 'utf8' });
|
|
541
|
-
assert.equal(nullLanePolicyCli.status, 2, `${nullLanePolicyCli.stdout}\n${nullLanePolicyCli.stderr}`);
|
|
542
|
-
assert.match(nullLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
|
|
543
|
-
|
|
544
|
-
const validatePolicyCli = spawnSync(process.execPath, [
|
|
545
|
-
POLICY_PATH,
|
|
546
|
-
'--validate-policy',
|
|
547
|
-
'--task-json',
|
|
548
|
-
JSON.stringify({ workflow_policy: forgedDirectAuth }),
|
|
549
|
-
'--json',
|
|
550
|
-
], { encoding: 'utf8' });
|
|
551
|
-
assert.equal(validatePolicyCli.status, 2, `${validatePolicyCli.stdout}\n${validatePolicyCli.stderr}`);
|
|
552
|
-
assert.equal(JSON.parse(validatePolicyCli.stdout).valid, false);
|
|
553
|
-
|
|
554
|
-
const validateNullPolicyCli = spawnSync(process.execPath, [
|
|
555
|
-
POLICY_PATH,
|
|
556
|
-
'--validate-policy',
|
|
557
|
-
'--task-json',
|
|
558
|
-
JSON.stringify({ workflow_policy: null }),
|
|
559
|
-
'--json',
|
|
560
|
-
], { encoding: 'utf8' });
|
|
561
|
-
assert.equal(validateNullPolicyCli.status, 2, `${validateNullPolicyCli.stdout}\n${validateNullPolicyCli.stderr}`);
|
|
562
|
-
assert.equal(JSON.parse(validateNullPolicyCli.stdout).valid, false);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
test('P1 workflow policy escalation is monotonic across all newly classified risks', () => {
|
|
566
|
-
const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
567
|
-
assert.equal(direct.lane, 'Direct');
|
|
568
|
-
assert.deepEqual(POLICY.escalateWorkflowPolicy(direct, {}), direct, 'no new risks must not escalate');
|
|
569
|
-
|
|
570
|
-
const standard = POLICY.escalateWorkflowPolicy(direct, { risks: ['new-standard-risk'], riskLevel: 'standard' });
|
|
571
|
-
assert.equal(standard.lane, 'Standard');
|
|
572
|
-
assert.equal(standard.artifact_profile, 'targeted');
|
|
573
|
-
assert.equal(standard.planning_depth, 'None');
|
|
574
|
-
assert.equal(standard.assurance_level, 'Elevated');
|
|
575
|
-
assert.equal(standard.risks.includes('new-standard-risk'), true);
|
|
576
|
-
assert.deepEqual(standard.proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
577
|
-
|
|
578
|
-
const ambiguityElevated = POLICY.escalateWorkflowPolicy(direct, { riskSignals: { ambiguity: true } });
|
|
579
|
-
assert.equal(ambiguityElevated.lane, 'Standard', 'risk discovery must require inspection without inventing an audit');
|
|
580
|
-
assert.equal(ambiguityElevated.assurance_level, 'Elevated');
|
|
581
|
-
|
|
582
|
-
const criticalSeverity = POLICY.escalateWorkflowPolicy(direct, { risks: ['payment'], riskLevel: 'critical' });
|
|
583
|
-
assert.equal(criticalSeverity.assurance_level, 'Elevated', 'severity labels must not create independent-audit authority');
|
|
584
|
-
assert.equal(criticalSeverity.proof_obligations.includes('needsIndependentAudit'), false);
|
|
585
|
-
|
|
586
|
-
const critical = POLICY.escalateWorkflowPolicy(standard, { risks: ['auth'], assurance_level: 'Strict' });
|
|
587
|
-
assert.equal(critical.lane, 'Critical');
|
|
588
|
-
assert.equal(critical.artifact_profile, 'targeted');
|
|
589
|
-
assert.equal(critical.planning_depth, 'None');
|
|
590
|
-
assert.equal(critical.assurance_level, 'Strict');
|
|
591
|
-
assert.equal(critical.automatic_assurance_level, 'Elevated');
|
|
592
|
-
assert.equal(critical.proof_obligations.includes('needsIndependentAudit'), true);
|
|
593
|
-
assert.equal(critical.proof_obligations.includes('needsResearchGrounding'), false);
|
|
594
|
-
|
|
595
|
-
const noDowngrade = POLICY.escalateWorkflowPolicy(critical, { risks: ['another-standard-risk'], riskLevel: 'standard' });
|
|
596
|
-
assert.equal(noDowngrade.lane, 'Critical');
|
|
597
|
-
assert.equal(noDowngrade.proof_obligations.includes('needsIndependentAudit'), true);
|
|
598
|
-
assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'), false);
|
|
599
|
-
|
|
600
|
-
const criticalAgain = POLICY.escalateWorkflowPolicy(critical, { risks: ['auth'] });
|
|
601
|
-
assert.deepEqual(criticalAgain, critical, 'repeating known risks must not create a new escalation');
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test('P1 legacy execution tier is read-compatible without becoming emitted policy authority', () => {
|
|
605
|
-
const legacy = { design_context: { execution_tier: 'Deep' } };
|
|
606
|
-
const snapshot = POLICY.workflowPolicySnapshot(legacy);
|
|
607
|
-
assert.equal(snapshot.lane, 'Critical');
|
|
608
|
-
assert.equal(snapshot.artifact_profile, 'strict');
|
|
609
|
-
const policy = POLICY.lanePolicy(legacy);
|
|
610
|
-
assert.equal(policy.lane, 'Critical');
|
|
611
|
-
assert.equal(Object.hasOwn(policy, 'execution_tier'), false);
|
|
612
|
-
assert.equal(Object.hasOwn(policy, 'executionTier'), false);
|
|
613
|
-
const legacyPlan = POLICY.delegationPlan({ tier: 'Deep' });
|
|
614
|
-
assert.equal(Object.hasOwn(legacyPlan, 'execution_tier'), false);
|
|
615
|
-
assert.equal(Object.hasOwn(legacyPlan, 'executionTier'), false);
|
|
616
|
-
assert.deepEqual(legacyPlan.proof_obligations, snapshot.proof_obligations);
|
|
617
|
-
assert.equal(POLICY.workflowPolicySnapshot({ design_context: { execution_tier: 'standard' } }).lane, 'Standard');
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
test('legacy v1 policy is read-compatible, immutable on read, and explicitly migrates to v2.1', () => {
|
|
621
|
-
const cases = [
|
|
622
|
-
{ lane: 'Direct', risks: [], obligations: ['needsExecutionProof'] },
|
|
623
|
-
{ lane: 'Standard', risks: [], obligations: ['needsInspection', 'needsExecutionProof'] },
|
|
624
|
-
{ lane: 'Critical', risks: ['auth'], obligations: ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding'] },
|
|
625
|
-
];
|
|
626
|
-
for (const fixture of cases) {
|
|
627
|
-
const v1 = {
|
|
628
|
-
version: '1',
|
|
629
|
-
lane: fixture.lane,
|
|
630
|
-
automatic_lane: fixture.lane,
|
|
631
|
-
risks: fixture.risks,
|
|
632
|
-
artifact_profile: { Direct: 'targeted', Standard: 'bounded', Critical: 'strict' }[fixture.lane],
|
|
633
|
-
proof_obligations: fixture.obligations,
|
|
634
|
-
actor_needs: POLICY.actorNeedsFor(fixture.obligations),
|
|
635
|
-
override_receipt: null,
|
|
636
|
-
};
|
|
637
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(v1).valid, true, fixture.lane);
|
|
638
|
-
const source = { feature_name: `legacy-${fixture.lane}`, workflow_policy: v1 };
|
|
639
|
-
const before = JSON.stringify(source);
|
|
640
|
-
const adapted = POLICY.readWorkflowPolicySnapshot(source);
|
|
641
|
-
assert.equal(adapted.version, '2');
|
|
642
|
-
assert.deepEqual(adapted.proof_obligations, fixture.obligations, fixture.lane);
|
|
643
|
-
assert.deepEqual(adapted.actor_needs, v1.actor_needs, fixture.lane);
|
|
644
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(adapted).valid, true, fixture.lane);
|
|
645
|
-
assert.equal(JSON.stringify(source), before, 'read adapter must not mutate persisted v1 state');
|
|
646
|
-
const migrated = POLICY.persistWorkflowPolicySnapshot(source).workflow_policy;
|
|
647
|
-
assert.equal(migrated.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
648
|
-
assert.deepEqual(Object.keys(migrated).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
649
|
-
const migratedView = POLICY.readWorkflowPolicySnapshot({ workflow_policy: migrated });
|
|
650
|
-
assert.equal(migratedView.lane, adapted.lane, fixture.lane);
|
|
651
|
-
assert.equal(migratedView.artifact_profile, adapted.artifact_profile, fixture.lane);
|
|
652
|
-
assert.deepEqual(migratedView.proof_obligations, POLICY.obligationsForAssurance(
|
|
653
|
-
migrated.assurance_level,
|
|
654
|
-
migrated.risks,
|
|
655
|
-
), fixture.lane);
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
test('post-classification downgrade is unsupported', () => {
|
|
660
|
-
assert.throws(
|
|
661
|
-
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
662
|
-
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
663
|
-
);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
test('caller-requested downgrade is blocked before persistence', () => {
|
|
667
|
-
assert.throws(
|
|
668
|
-
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
669
|
-
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
670
|
-
);
|
|
671
|
-
// also via CLI
|
|
672
|
-
const cli = spawnSync(process.execPath, [
|
|
673
|
-
POLICY_PATH,
|
|
674
|
-
'--classify-lane',
|
|
675
|
-
'--task-json',
|
|
676
|
-
JSON.stringify({ riskSignals: { auth: true }, override: 'Direct' }),
|
|
677
|
-
'--json',
|
|
678
|
-
], { encoding: 'utf8' });
|
|
679
|
-
assert.equal(cli.status, 2);
|
|
680
|
-
assert.match(cli.stderr, /planning_depth downgrade.*not permitted/i);
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
test('forged approval via legacy approved field is rejected', () => {
|
|
684
|
-
assert.throws(() => POLICY.approvalState({ generated: true, approved: true }), /Legacy approval state/);
|
|
685
|
-
const cli = spawnSync(process.execPath, [
|
|
686
|
-
POLICY_PATH,
|
|
687
|
-
'--approval-state',
|
|
688
|
-
'--task-json',
|
|
689
|
-
JSON.stringify({ generated: true, approved: true }),
|
|
690
|
-
'--json',
|
|
691
|
-
], { encoding: 'utf8' });
|
|
692
|
-
assert.equal(cli.status, 2);
|
|
693
|
-
assert.match(cli.stderr, /Legacy approval state/);
|
|
694
|
-
const legacyCheck = POLICY.validateApprovalSchema({ approvals: { requirements: { generated: true, approved: true } } });
|
|
695
|
-
assert.equal(legacyCheck.valid, false);
|
|
696
|
-
assert.equal(legacyCheck.legacy, true);
|
|
697
|
-
assert.match(legacyCheck.error, /Legacy/);
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
test('technical approval readiness ignores legacy user_approved', () => {
|
|
701
|
-
const state = POLICY.approvalState({ generated: true, agent_validated: true });
|
|
702
|
-
assert.equal(state.generated, true);
|
|
703
|
-
assert.equal(state.agent_validated, true);
|
|
704
|
-
assert.equal(state.ready, true);
|
|
705
|
-
assert.equal(state.schema_version, '2.0');
|
|
706
|
-
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true }).ready, true);
|
|
707
|
-
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false }).ready, true);
|
|
708
|
-
const cli = spawnSync(process.execPath, [
|
|
709
|
-
POLICY_PATH,
|
|
710
|
-
'--approval-state',
|
|
711
|
-
'--task-json',
|
|
712
|
-
JSON.stringify({ generated: true, agent_validated: true }),
|
|
713
|
-
'--json',
|
|
714
|
-
], { encoding: 'utf8' });
|
|
715
|
-
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
716
|
-
assert.equal(JSON.parse(cli.stdout).state.ready, true);
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
test('approval schema fails closed for explicit null, array, empty, and malformed states', () => {
|
|
720
|
-
const absent = POLICY.validateApprovalSchema({});
|
|
721
|
-
assert.equal(absent.valid, true);
|
|
722
|
-
assert.equal(absent.absent, true);
|
|
723
|
-
assert.equal(POLICY.approvalState({}).present, false);
|
|
724
|
-
|
|
725
|
-
const invalidSpecs = [
|
|
726
|
-
[],
|
|
727
|
-
{ approvals: null },
|
|
728
|
-
{ approvals: [] },
|
|
729
|
-
{ approvals: {} },
|
|
730
|
-
{ approvals: { requirements: null } },
|
|
731
|
-
{ approvals: { requirements: { generated: true, agent_validated: true, user_approved: null } } },
|
|
732
|
-
{ approvals: { requirements: { generated: true, agent_validated: true, user_approved: true, extra: false } } },
|
|
733
|
-
];
|
|
734
|
-
for (const spec of invalidSpecs) {
|
|
735
|
-
assert.equal(POLICY.validateApprovalSchema(spec).valid, false, `invalid approval should fail: ${JSON.stringify(spec)}`);
|
|
736
|
-
assert.throws(() => POLICY.approvalState(spec), /approval|approvals|schema/i);
|
|
737
|
-
}
|
|
738
|
-
assert.throws(() => POLICY.approvalState({ schema_version: null }), /schema_version/);
|
|
739
|
-
assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
test('CLI exposes a derived lane without making it primary Develop authority', () => {
|
|
743
|
-
const cli = spawnSync(process.execPath, [
|
|
744
|
-
POLICY_PATH,
|
|
745
|
-
'--classify-lane',
|
|
746
|
-
'--task-json',
|
|
747
|
-
JSON.stringify({ reversible: true, lowRisk: true, isolated: true }),
|
|
748
|
-
'--json',
|
|
749
|
-
], { encoding: 'utf8' });
|
|
750
|
-
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
751
|
-
const payload = JSON.parse(cli.stdout);
|
|
752
|
-
assert.equal(payload.classification.lane, 'Direct');
|
|
753
|
-
assert.equal(payload.policy.requiresSpec, false);
|
|
211
|
+
test('Develop names the process-first packet as its only authority', () => {
|
|
754
212
|
const develop = read(DEVELOP);
|
|
755
213
|
const regions = markdownLegacyRegions(develop);
|
|
756
214
|
assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
|
|
@@ -764,18 +222,6 @@ test('CLI exposes a derived lane without making it primary Develop authority', (
|
|
|
764
222
|
assert.match(develop, /--notes.*opt-in/i);
|
|
765
223
|
assert.doesNotMatch(develop, /--no-notes/);
|
|
766
224
|
assert.doesNotMatch(develop, /planner|implementer|test-runner|code-auditor|docs-keeper/i);
|
|
767
|
-
const fullRoutine = POLICY.readWorkflowPolicySnapshot({
|
|
768
|
-
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Routine' }),
|
|
769
|
-
});
|
|
770
|
-
const compactStrict = POLICY.readWorkflowPolicySnapshot({
|
|
771
|
-
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' }),
|
|
772
|
-
});
|
|
773
|
-
assert.equal(fullRoutine.planning_depth, 'Full');
|
|
774
|
-
assert.equal(fullRoutine.assurance_level, 'Routine');
|
|
775
|
-
assert.equal(compactStrict.planning_depth, 'Compact');
|
|
776
|
-
assert.equal(compactStrict.assurance_level, 'Strict');
|
|
777
|
-
assert.notEqual(fullRoutine.artifact_profile, compactStrict.artifact_profile);
|
|
778
|
-
assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
|
|
779
225
|
});
|
|
780
226
|
|
|
781
227
|
test('Specs primary output is a flat process-first packet with isolated legacy compatibility', () => {
|
|
@@ -1107,7 +553,7 @@ test('Claude installed Test preserves plan-native proof and references', () => {
|
|
|
1107
553
|
const installedReview = fs.readFileSync(path.join(root, '.claude/skills/code-review/SKILL.md'), 'utf8');
|
|
1108
554
|
assert.equal(installedReview, fs.readFileSync(CODE_REVIEW_SKILL, 'utf8'));
|
|
1109
555
|
assert.match(installedReview, /controller-validated `test-proof-v1`/);
|
|
1110
|
-
assert.match(installedReview, /
|
|
556
|
+
assert.match(installedReview, /For proof consumption, this skill's `## Execution-proof boundary` is\s+authoritative/i);
|
|
1111
557
|
assert.equal(fs.existsSync(path.join(root, '.agents/skills/test')), false);
|
|
1112
558
|
} finally {
|
|
1113
559
|
fs.rmSync(root, { recursive: true, force: true });
|
|
@@ -1121,411 +567,24 @@ test('specs-usage-guide documents Test proof handoff without timing claims', ()
|
|
|
1121
567
|
assert.match(section, /Test không ghi `Status:` hay inline[\s\S]*Develop controller là writer duy nhất/i);
|
|
1122
568
|
assert.match(section, /tracked\/untracked\/ignored drift/i);
|
|
1123
569
|
assert.match(section, /HTTPS\/localhost origin[\s\S]*fresh consent/i);
|
|
1124
|
-
assert.match(section, /
|
|
570
|
+
assert.match(section, /chỉ nằm trong `## Receipt` inline/i);
|
|
1125
571
|
assert.match(section, /không phải benchmark thời gian/i);
|
|
1126
572
|
assert.match(section, /không chứng minh live adherence/i);
|
|
1127
573
|
assert.doesNotMatch(section, /\b\d+(?:\.\d+)?\s*(?:ms|milliseconds?|seconds?|minutes?)\b/i);
|
|
1128
574
|
assert.doesNotMatch(section, /\[(?:LIVE_)?VERIFIED\]/i);
|
|
1129
575
|
});
|
|
1130
576
|
|
|
1131
|
-
test('canonical verdict adapter handles completion and unfinished decisions', () => {
|
|
1132
|
-
assert.deepEqual(POLICY.REVIEW_VERDICTS, ['PASS', 'PASS_WITH_WARNINGS', 'FAIL', 'BLOCKED']);
|
|
1133
|
-
assert.doesNotThrow(() => POLICY.assertVerdict('PASS_WITH_WARNINGS'));
|
|
1134
|
-
assert.throws(() => POLICY.assertVerdict('PARTIAL'), /Unsupported canonical verdict/);
|
|
1135
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS').completion, 'unfinished');
|
|
1136
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS').unfinished, true);
|
|
1137
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS').createsCompletion, false);
|
|
1138
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS_WITH_WARNINGS').unfinished, true);
|
|
1139
|
-
assert.equal(POLICY.consumeReviewVerdict('FAIL').retry, true);
|
|
1140
|
-
assert.equal(POLICY.consumeReviewVerdict('BLOCKED').retry, false);
|
|
1141
|
-
assert.equal(POLICY.consumeReviewVerdict('BLOCKED').unfinished, true);
|
|
1142
|
-
assert.equal(POLICY.consumeReviewVerdict('PARTIAL').completion, 'unfinished');
|
|
1143
|
-
assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').completion, 'unfinished');
|
|
1144
|
-
assert.equal(POLICY.consumeReviewVerdict('PARTIAL').verdict, 'BLOCKED');
|
|
1145
|
-
assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').verdict, 'BLOCKED');
|
|
1146
|
-
assert.equal(POLICY.consumeReviewVerdict('PARTIAL').retry, false);
|
|
1147
|
-
assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').retry, false);
|
|
1148
|
-
assert.throws(() => POLICY.consumeReviewVerdict('UNKNOWN'), /Unsupported canonical verdict/);
|
|
1149
|
-
const cli = spawnSync(process.execPath, [POLICY_PATH, '--consume-verdict', '--verdict', 'BLOCKED', '--json'], { encoding: 'utf8' });
|
|
1150
|
-
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
1151
|
-
assert.deepEqual(JSON.parse(cli.stdout).action, 'stop');
|
|
1152
|
-
assert.match(read(GATE), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
1153
|
-
assert.match(read(CODE_REVIEW_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
1154
|
-
assert.match(read(TEST_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
test('completion decision requires canonical execution receipt and every workflow obligation', () => {
|
|
1158
|
-
const receipt = canonicalReceipt();
|
|
1159
|
-
const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
|
|
1160
|
-
const audit = {
|
|
1161
|
-
schema_version: '1',
|
|
1162
|
-
reviewer_session_id: 'review-session-1',
|
|
1163
|
-
implementation_session_id: 'implementation-session-1',
|
|
1164
|
-
expected_provenance: { ...EXPECTED_PROVENANCE },
|
|
1165
|
-
evidence: 'independent audit report for the bound change',
|
|
1166
|
-
verdict: 'PASS',
|
|
1167
|
-
};
|
|
1168
|
-
const missing = POLICY.completionDecision('PASS', {
|
|
1169
|
-
workflow_policy: critical,
|
|
1170
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1171
|
-
execution_receipt: receipt,
|
|
1172
|
-
proofs: {
|
|
1173
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1174
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1175
|
-
},
|
|
1176
|
-
});
|
|
1177
|
-
assert.equal(missing.completion, 'unfinished');
|
|
1178
|
-
assert.ok(missing.missingProof.includes('needsIndependentAudit'));
|
|
1179
|
-
assert.equal(missing.unfinished, true);
|
|
1180
|
-
|
|
1181
|
-
const complete = POLICY.completionDecision('PASS', {
|
|
1182
|
-
workflow_policy: critical,
|
|
1183
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1184
|
-
execution_receipt: receipt,
|
|
1185
|
-
proofs: {
|
|
1186
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1187
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1188
|
-
needsIndependentAudit: audit,
|
|
1189
|
-
},
|
|
1190
|
-
});
|
|
1191
|
-
assert.equal(complete.completion, 'complete');
|
|
1192
|
-
assert.equal(complete.unfinished, false);
|
|
1193
|
-
assert.equal(complete.warnings, undefined);
|
|
1194
|
-
const warnings = POLICY.completionDecision('PASS_WITH_WARNINGS', {
|
|
1195
|
-
workflow_policy: critical,
|
|
1196
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1197
|
-
execution_receipt: receipt,
|
|
1198
|
-
proofs: {
|
|
1199
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1200
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1201
|
-
needsIndependentAudit: audit,
|
|
1202
|
-
},
|
|
1203
|
-
});
|
|
1204
|
-
assert.equal(warnings.completion, 'unfinished');
|
|
1205
|
-
assert.equal(warnings.unfinished, true);
|
|
1206
|
-
assert.match(warnings.blocker, /literal PASS/);
|
|
1207
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1208
|
-
workflow_policy: { proof_obligations: ['needsExecutionProof'] },
|
|
1209
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1210
|
-
execution_receipt: receipt,
|
|
1211
|
-
}).completion, 'unfinished');
|
|
1212
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1213
|
-
workflow_policy: {},
|
|
1214
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1215
|
-
execution_receipt: receipt,
|
|
1216
|
-
}).completion, 'unfinished');
|
|
1217
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1218
|
-
workflow_policy: critical,
|
|
1219
|
-
execution_receipt: receipt,
|
|
1220
|
-
}).completion, 'unfinished', 'arbitrary Base/Head without runtime binding must not complete');
|
|
1221
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1222
|
-
workflow_policy: critical,
|
|
1223
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1224
|
-
execution_receipt: receipt,
|
|
1225
|
-
proofs: {
|
|
1226
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1227
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1228
|
-
needsIndependentAudit: { independent: true, evidence: 'marker only', verdict: 'PASS' },
|
|
1229
|
-
},
|
|
1230
|
-
}).completion, 'unfinished', 'independent marker must not satisfy audit');
|
|
1231
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ independent: true }, EXPECTED_PROVENANCE).valid, false);
|
|
1232
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, verdict: 'PASS_WITH_WARNINGS' }, EXPECTED_PROVENANCE).valid, false);
|
|
1233
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, reviewer_session_id: audit.implementation_session_id }, EXPECTED_PROVENANCE).valid, false);
|
|
1234
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, expected_provenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }, EXPECTED_PROVENANCE).valid, false);
|
|
1235
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, extra: 'forged' }, EXPECTED_PROVENANCE).valid, false);
|
|
1236
|
-
assert.equal(POLICY.validateIndependentAuditEvidence([audit], EXPECTED_PROVENANCE).valid, false);
|
|
1237
|
-
assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, RUNTIME_CONTEXT).valid, true);
|
|
1238
|
-
assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, { ...RUNTIME_CONTEXT }).valid, false, 'forged runtime session context must not satisfy audit binding');
|
|
1239
|
-
assert.equal(POLICY.completionDecision('PASS', {}).completion, 'unfinished');
|
|
1240
|
-
const artifactContext = {
|
|
1241
|
-
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
|
|
1242
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1243
|
-
task_context: { artifacts: ['output/bundle.js'], expected_provenance: { ...EXPECTED_PROVENANCE } },
|
|
1244
|
-
execution_receipt: receipt,
|
|
1245
|
-
proofs: { needsInspection: { evidence: 'inspection receipt' } },
|
|
1246
|
-
receipt_options: { requireArtifactHash: false, artifactPaths: [] },
|
|
1247
|
-
};
|
|
1248
|
-
const artifactBlocked = POLICY.completionDecision('PASS', artifactContext);
|
|
1249
|
-
assert.equal(artifactBlocked.completion, 'unfinished', 'caller receipt options must not weaken task artifact requirements');
|
|
1250
|
-
assert.ok(artifactBlocked.missingProof.some((item) => item.includes('artifact_hash')));
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
test('auto authoring reaches readiness only after promotion, validation, grounding, and current review', () => {
|
|
1254
|
-
const fixture = createCanonicalAuthoringFixture();
|
|
1255
|
-
try {
|
|
1256
|
-
let spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1257
|
-
assert.equal(spec.semantic_model, null);
|
|
1258
|
-
spec.ready_for_implementation = true;
|
|
1259
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1260
|
-
let validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
1261
|
-
assert.ok(validation.errors.some((error) => error.includes('requires explicit promoted machine semantic authority')));
|
|
1262
|
-
|
|
1263
|
-
spec.ready_for_implementation = false;
|
|
1264
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1265
|
-
promoteCanonicalModel(fixture);
|
|
1266
|
-
spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1267
|
-
assert.ok(spec.semantic_model && spec.semantic_model.criteria.length === 2);
|
|
1268
|
-
spec.ready_for_implementation = true;
|
|
1269
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1270
|
-
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
1271
|
-
assert.ok(validation.errors.some((error) => error.includes('readiness requires validated')));
|
|
1272
|
-
assert.ok(validation.errors.some((error) => error.includes('requires completed semantic review')));
|
|
1273
|
-
|
|
1274
|
-
spec.ready_for_implementation = false;
|
|
1275
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1276
|
-
spec = makeCanonicalReady(fixture);
|
|
1277
|
-
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
1278
|
-
assert.deepEqual(validation.errors, [], validation.errors.join('\n'));
|
|
1279
|
-
assert.equal(GROUNDER.groundSpec({ specDir: fixture.specDir, root: fixture.root, spec }).errors.length, 0);
|
|
1280
|
-
|
|
1281
|
-
const specs = read(SPECS);
|
|
1282
|
-
const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
|
|
1283
|
-
assert.ok(executableBlocks.every((block) => !/cf:?develop/i.test(block)), 'Specs must never execute Develop');
|
|
1284
|
-
assert.match(specs, /An implementation workflow owns execution/i);
|
|
1285
|
-
assert.match(specs, /selects one unblocked task[\s\S]*runs\s+the task's verification[\s\S]*inline `## Receipt`/i);
|
|
1286
|
-
assert.match(specs, /The user decides\s+at GATE-DONE whether the feature is done/i);
|
|
1287
|
-
} finally {
|
|
1288
|
-
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
1289
|
-
}
|
|
1290
|
-
});
|
|
1291
|
-
|
|
1292
|
-
test('atomic readiness supports one-criterion Routine specs and binds semantic text', () => {
|
|
1293
|
-
const fixture = createOrdinaryAuthoringFixture();
|
|
1294
|
-
try {
|
|
1295
|
-
const authored = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1296
|
-
authored.decisions = [
|
|
1297
|
-
{ 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.' },
|
|
1298
|
-
{ 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.' },
|
|
1299
|
-
];
|
|
1300
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(authored, null, 2)}\n`);
|
|
1301
|
-
const result = READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() });
|
|
1302
|
-
assert.equal(result.spec.ready_for_implementation, true);
|
|
1303
|
-
assert.equal(result.spec.semantic_model.criteria.length, 1);
|
|
1304
|
-
assert.match(result.spec.semantic_model.criteria[0].text, /delete the selected record/);
|
|
1305
|
-
assert.deepEqual(VALIDATOR.validateSpec(fixture.specDir).errors, []);
|
|
1306
|
-
|
|
1307
|
-
const beforeDigest = result.semantic_digest;
|
|
1308
|
-
const design = path.join(fixture.specDir, 'design.md');
|
|
1309
|
-
const originalDesign = fs.readFileSync(design, 'utf8');
|
|
1310
|
-
fs.writeFileSync(design, originalDesign.replace('deleted rather than archived', 'archived rather than deleted'));
|
|
1311
|
-
const decisionProjection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
1312
|
-
assert.notDeepEqual(decisionProjection.model.design_records, result.spec.semantic_model.design_records);
|
|
1313
|
-
const decisionDigest = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: decisionProjection.model });
|
|
1314
|
-
assert.equal(decisionDigest.errors.length, 0, decisionDigest.errors.join('\n'));
|
|
1315
|
-
assert.notEqual(decisionDigest.digest, beforeDigest);
|
|
1316
|
-
fs.writeFileSync(design, originalDesign);
|
|
1317
|
-
|
|
1318
|
-
const requirements = path.join(fixture.specDir, 'requirements.md');
|
|
1319
|
-
fs.writeFileSync(requirements, fs.readFileSync(requirements, 'utf8').replace('delete the selected record', 'archive the selected record'));
|
|
1320
|
-
const projection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
1321
|
-
assert.notEqual(projection.model.criteria[0].text, result.spec.semantic_model.criteria[0].text);
|
|
1322
|
-
const changed = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: projection.model });
|
|
1323
|
-
assert.equal(changed.errors.length, 0, changed.errors.join('\n'));
|
|
1324
|
-
assert.notEqual(changed.digest, beforeDigest);
|
|
1325
|
-
assert.ok(VALIDATOR.validateSpec(fixture.specDir).errors.some((error) => /projection|stale/.test(error)));
|
|
1326
|
-
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1329
|
-
test('atomic readiness preserves exact bytes on review, grounding, decision, and Strict authority failures', () => {
|
|
1330
|
-
const fixture = createOrdinaryAuthoringFixture();
|
|
1331
|
-
try {
|
|
1332
|
-
const original = fs.readFileSync(fixture.specPath);
|
|
1333
|
-
for (const mutate of [
|
|
1334
|
-
(review) => ({ ...review, reviewed_criteria: [] }),
|
|
1335
|
-
(review) => ({ ...review, counterexamples: [] }),
|
|
1336
|
-
]) {
|
|
1337
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: mutate(ordinaryReview()) }), /readiness validation/);
|
|
1338
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
1339
|
-
}
|
|
1340
|
-
fs.unlinkSync(path.join(fixture.root, 'src', 'entry.js'));
|
|
1341
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /grounding|validation/);
|
|
1342
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
1343
|
-
fs.writeFileSync(path.join(fixture.root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
1344
|
-
|
|
1345
|
-
process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME = '1';
|
|
1346
|
-
try {
|
|
1347
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /injected finalization failure/);
|
|
1348
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
1349
|
-
} finally { delete process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME; }
|
|
1350
|
-
|
|
1351
|
-
const blocked = JSON.parse(original);
|
|
1352
|
-
blocked.decisions = [{ id: 'Q1', classification: 'user_owned', statement: 'Choose permanent deletion or retention archive behavior.', status: 'unresolved', evidence: null }];
|
|
1353
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
1354
|
-
const blockedBytes = fs.readFileSync(fixture.specPath);
|
|
1355
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /unresolved material user-owned decision/);
|
|
1356
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(blockedBytes), true);
|
|
1357
|
-
|
|
1358
|
-
delete blocked.decisions;
|
|
1359
|
-
blocked.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
1360
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
1361
|
-
const strictBytes = fs.readFileSync(fixture.specPath);
|
|
1362
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict assurance is no longer supported/);
|
|
1363
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(strictBytes), true);
|
|
1364
|
-
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
test('Strict readiness is refused as no longer supported', () => {
|
|
1368
|
-
const fixture = createOrdinaryAuthoringFixture();
|
|
1369
|
-
try {
|
|
1370
|
-
const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1371
|
-
spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
1372
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1373
|
-
const before = fs.readFileSync(fixture.specPath);
|
|
1374
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict assurance is no longer supported/);
|
|
1375
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(before), true);
|
|
1376
|
-
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
|
-
test('done is a final-state request bound to current semantic model and digest', () => {
|
|
1380
|
-
const fixture = createCanonicalAuthoringFixture();
|
|
1381
|
-
try {
|
|
1382
|
-
promoteCanonicalModel(fixture);
|
|
1383
|
-
const spec = makeCanonicalReady(fixture);
|
|
1384
|
-
spec.status = 'done';
|
|
1385
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1386
|
-
const candidate = {
|
|
1387
|
-
featureDir: fixture.specDir,
|
|
1388
|
-
featureName: 'feature',
|
|
1389
|
-
specFile: fixture.specPath,
|
|
1390
|
-
spec,
|
|
1391
|
-
};
|
|
1392
|
-
const dependencies = () => ({
|
|
1393
|
-
validator: VALIDATOR,
|
|
1394
|
-
grounder: GROUNDER,
|
|
1395
|
-
semanticAuthority: {},
|
|
1396
|
-
});
|
|
1397
|
-
const current = FINAL_STATE.validateCanonicalFinalState({
|
|
1398
|
-
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1399
|
-
});
|
|
1400
|
-
assert.equal(current.ok, true, current.reason);
|
|
1401
|
-
assert.equal(current.semanticDigest, spec.validation.semantic_review.semantic_digest);
|
|
1402
|
-
|
|
1403
|
-
const failedReceipt = structuredClone(spec);
|
|
1404
|
-
failedReceipt.validation.semantic_review.verdict = 'FAIL';
|
|
1405
|
-
const receiptMutation = FINAL_STATE.validateCanonicalFinalState({
|
|
1406
|
-
policy: POLICY,
|
|
1407
|
-
projectRoot: fixture.root,
|
|
1408
|
-
candidate: { ...candidate, spec: failedReceipt },
|
|
1409
|
-
dependencies,
|
|
1410
|
-
});
|
|
1411
|
-
assert.equal(receiptMutation.ok, false);
|
|
1412
|
-
assert.match(receiptMutation.reason, /PASS CONTINUE/);
|
|
1413
|
-
|
|
1414
|
-
const mismatchedHistory = structuredClone(spec);
|
|
1415
|
-
const latest = mismatchedHistory.validation.semantic_review_history.entries[
|
|
1416
|
-
mismatchedHistory.validation.semantic_review_history.entries.length - 1
|
|
1417
|
-
];
|
|
1418
|
-
latest.review_receipt_digest = `sha256:${'0'.repeat(64)}`;
|
|
1419
|
-
const digestMismatch = FINAL_STATE.validateCanonicalFinalState({
|
|
1420
|
-
policy: POLICY,
|
|
1421
|
-
projectRoot: fixture.root,
|
|
1422
|
-
candidate: { ...candidate, spec: mismatchedHistory },
|
|
1423
|
-
dependencies,
|
|
1424
|
-
});
|
|
1425
|
-
assert.equal(digestMismatch.ok, false);
|
|
1426
|
-
assert.match(digestMismatch.reason, /review_receipt_digest|canonical receipt/);
|
|
1427
|
-
|
|
1428
|
-
const requirementsPath = path.join(fixture.specDir, 'requirements.md');
|
|
1429
|
-
const requirements = fs.readFileSync(requirementsPath, 'utf8');
|
|
1430
|
-
fs.writeFileSync(requirementsPath, requirements.replace('invalid input shall return rejected state', 'invalid input shall return a deterministic rejected state'));
|
|
1431
|
-
const staleDigest = FINAL_STATE.validateCanonicalFinalState({
|
|
1432
|
-
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1433
|
-
});
|
|
1434
|
-
assert.equal(staleDigest.ok, false);
|
|
1435
|
-
assert.match(staleDigest.reason, /stale|differs from Markdown projection/i);
|
|
1436
|
-
|
|
1437
|
-
fs.writeFileSync(requirementsPath, requirements);
|
|
1438
|
-
const staleModel = structuredClone(spec);
|
|
1439
|
-
staleModel.semantic_model.criteria[0].owner = 'A-D-02';
|
|
1440
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(staleModel, null, 2)}\n`);
|
|
1441
|
-
const invalidModel = FINAL_STATE.validateCanonicalFinalState({
|
|
1442
|
-
policy: POLICY,
|
|
1443
|
-
projectRoot: fixture.root,
|
|
1444
|
-
candidate: { ...candidate, spec: staleModel },
|
|
1445
|
-
dependencies,
|
|
1446
|
-
});
|
|
1447
|
-
assert.equal(invalidModel.ok, false);
|
|
1448
|
-
assert.match(invalidModel.reason, /semantic projection|semantic_model|validation failed/i);
|
|
1449
|
-
} finally {
|
|
1450
|
-
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
1451
|
-
}
|
|
1452
|
-
});
|
|
1453
|
-
|
|
1454
|
-
test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
|
|
1455
|
-
const initial = canonicalFlashTask();
|
|
1456
|
-
for (const verdict of ['FAIL', 'BLOCKED', 'PARTIAL', 'NO_TESTS', 'PASS_WITH_WARNINGS']) {
|
|
1457
|
-
const result = POLICY.promoteFlashTask(initial, verdict);
|
|
1458
|
-
assert.equal(result.status, 'in_progress');
|
|
1459
|
-
assert.equal(result.receipt, 'FLASH_UNVERIFIED');
|
|
1460
|
-
assert.equal(result.dependencyBlocked, true);
|
|
1461
|
-
assert.equal(result.unblocks, false);
|
|
1462
|
-
assert.match(result.blocker, /verification|test proof/);
|
|
1463
|
-
}
|
|
1464
|
-
const promoted = POLICY.promoteFlashTask(initial, 'PASS', canonicalReceipt());
|
|
1465
|
-
assert.equal(promoted.status, 'in_progress');
|
|
1466
|
-
assert.equal(promoted.receipt.startsWith('Verification: PASS'), true);
|
|
1467
|
-
assert.equal(promoted.blocker, null);
|
|
1468
|
-
assert.equal(promoted.dependencyBlocked, true);
|
|
1469
|
-
assert.equal(promoted.unblocks, false);
|
|
1470
|
-
assert.equal(promoted.readyForSync, true);
|
|
1471
|
-
assert.equal(POLICY.finalizeFlashTask(promoted, 'sync'), promoted);
|
|
1472
|
-
const forgedFinalize = POLICY.finalizeFlashTask(promoted, 'sync-finalize');
|
|
1473
|
-
assert.equal(forgedFinalize.status, 'in_progress');
|
|
1474
|
-
assert.equal(forgedFinalize.dependencyBlocked, true);
|
|
1475
|
-
assert.equal(forgedFinalize.unblocks, false);
|
|
1476
|
-
const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', promoted.receipt);
|
|
1477
|
-
assert.equal(finalized.status, 'done');
|
|
1478
|
-
assert.equal(finalized.dependencyBlocked, false);
|
|
1479
|
-
assert.equal(finalized.unblocks, true);
|
|
1480
|
-
assert.equal(finalized.readyForSync, false);
|
|
1481
|
-
assert.equal(POLICY.isStaleFlashDone({ status: 'done', receipt: 'FLASH_UNVERIFIED' }), true);
|
|
1482
|
-
assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/i);
|
|
1483
|
-
assert.match(read(SYNC_SKILL), /sync-finalize/);
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
|
|
1487
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-spec-gate-'));
|
|
1488
|
-
const specGate = installClaudeRuntimeClosure(root);
|
|
1489
|
-
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'spec-final-state.cjs')), true);
|
|
1490
|
-
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'validate-spec-output.cjs')), true);
|
|
1491
|
-
fs.mkdirSync(path.join(root, 'specs', 'demo', 'tasks'), { recursive: true });
|
|
1492
|
-
fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
|
|
1493
|
-
status: 'in_progress',
|
|
1494
|
-
current_phase: 'closeout',
|
|
1495
|
-
feature_name: 'demo',
|
|
1496
|
-
task_registry: { 'tasks/task.md': { status: 'done', receipt: 'FLASH_UNVERIFIED' } },
|
|
1497
|
-
}));
|
|
1498
|
-
initFixtureGit(root);
|
|
1499
|
-
try {
|
|
1500
|
-
const result = spawnSync(process.execPath, [specGate], {
|
|
1501
|
-
cwd: root,
|
|
1502
|
-
env: { ...process.env, PROJECT_ROOT: root },
|
|
1503
|
-
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
1504
|
-
encoding: 'utf8',
|
|
1505
|
-
});
|
|
1506
|
-
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
1507
|
-
const payload = JSON.parse(result.stdout);
|
|
1508
|
-
assert.equal(payload.decision, 'block');
|
|
1509
|
-
assert.match(payload.reason, /FLASH_UNVERIFIED/);
|
|
1510
|
-
} finally {
|
|
1511
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
1512
|
-
}
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
577
|
test('first-run cache bypass is blocked - canonical receipt required even without cache', () => {
|
|
1516
578
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-first-run-'));
|
|
1517
579
|
const claude = path.join(root, '.claude');
|
|
1518
580
|
const specGate = installClaudeRuntimeClosure(root);
|
|
1519
|
-
const
|
|
1520
|
-
fs.mkdirSync(
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
task_registry: { 'tasks/task-R1-01-one.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
|
|
1527
|
-
}));
|
|
1528
|
-
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');
|
|
581
|
+
const featureDir = path.join(root, 'specs', 'demo');
|
|
582
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
583
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo\nSpecs-Contract: process-first-ready-v1\n');
|
|
584
|
+
const taskFile = path.join(featureDir, 'task-01-one.md');
|
|
585
|
+
const head = '# Task 01: One\n\nStatus: done\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- Command: pnpm test\n\n';
|
|
586
|
+
// Done task without a canonical receipt (no Command, no provenance)
|
|
587
|
+
fs.writeFileSync(taskFile, `${head}## Receipt\n\nVerification: PASS\n\`\`\`\nnpm test\n\`\`\`\n`);
|
|
1529
588
|
initFixtureGit(root);
|
|
1530
589
|
try {
|
|
1531
590
|
assert.equal(fs.existsSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')), false);
|
|
@@ -1538,22 +597,18 @@ test('first-run cache bypass is blocked - canonical receipt required even withou
|
|
|
1538
597
|
assert.equal(result.status, 0);
|
|
1539
598
|
const payload = JSON.parse(result.stdout);
|
|
1540
599
|
assert.equal(payload.decision, 'block', 'first-run without canonical receipt must block');
|
|
1541
|
-
assert.match(payload.reason, /
|
|
600
|
+
assert.match(payload.reason, /task-01-one\.md/);
|
|
1542
601
|
assert.match(payload.reason, /receipt|command|provenance/i);
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
);
|
|
1550
|
-
fs.writeFileSync(
|
|
1551
|
-
path.join(root, 'specs', 'demo', 'feature-receipt.md'),
|
|
1552
|
-
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'),
|
|
1553
|
-
);
|
|
602
|
+
const context = PROVENANCE_HELPER.deriveRuntimeContext({
|
|
603
|
+
projectRoot: root,
|
|
604
|
+
specsRoot: path.join(root, 'specs'),
|
|
605
|
+
specFile: path.join(featureDir, 'plan.md'),
|
|
606
|
+
featureName: 'demo',
|
|
607
|
+
runtimeSession: 'test',
|
|
608
|
+
});
|
|
609
|
+
fs.writeFileSync(taskFile, `${head}## Receipt\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: ${context.base}\nHead: ${context.head}\n\`\`\`\npnpm test\nPASS\n\`\`\`\n`);
|
|
1554
610
|
// Clear cache to simulate fresh first-run with valid receipt
|
|
1555
611
|
try { fs.unlinkSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')); } catch {}
|
|
1556
|
-
// Re-run with valid receipt - should not block
|
|
1557
612
|
const result2 = spawnSync(process.execPath, [specGate], {
|
|
1558
613
|
cwd: root,
|
|
1559
614
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
@@ -1576,10 +631,6 @@ test('canonical receipt requires command, exit, provenance and unambiguous PASS'
|
|
|
1576
631
|
assert.ok(missingVerification.includes('verification_state'));
|
|
1577
632
|
const artifactWithoutHash = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: dist/bundle.js\n');
|
|
1578
633
|
assert.ok(artifactWithoutHash.includes('artifact_hash'));
|
|
1579
|
-
// CLI validation
|
|
1580
|
-
const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n' }), '--json'], { encoding: 'utf8' });
|
|
1581
|
-
assert.equal(cli.status, 0);
|
|
1582
|
-
assert.equal(JSON.parse(cli.stdout).ok, true);
|
|
1583
634
|
});
|
|
1584
635
|
|
|
1585
636
|
test('canonical receipt provenance requires both Base and Head (or both base_sha and head_sha)', () => {
|
|
@@ -1629,27 +680,10 @@ test('canonical receipt provenance requires both Base and Head (or both base_sha
|
|
|
1629
680
|
assert.deepEqual(validWithValues, [], 'valid Base and Head with values should pass');
|
|
1630
681
|
const validShaWithValues = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: 0123456789abcdef0123456789abcdef01234567\nhead_sha: 89abcdef0123456789abcdef0123456789abcdef\n');
|
|
1631
682
|
assert.deepEqual(validShaWithValues, [], 'valid base_sha and head_sha with values should pass');
|
|
1632
|
-
// CLI: only Base via --validate-receipt should fail
|
|
1633
|
-
const cliOnlyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\n' }), '--json'], { encoding: 'utf8' });
|
|
1634
|
-
assert.equal(cliOnlyBase.status, 2);
|
|
1635
|
-
assert.equal(JSON.parse(cliOnlyBase.stdout).ok, false);
|
|
1636
|
-
assert.ok(JSON.parse(cliOnlyBase.stdout).failures.includes('provenance'));
|
|
1637
|
-
const cliBoth = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n' }), '--json'], { encoding: 'utf8' });
|
|
1638
|
-
assert.equal(cliBoth.status, 0);
|
|
1639
|
-
assert.equal(JSON.parse(cliBoth.stdout).ok, true);
|
|
1640
|
-
// CLI empty Base should fail
|
|
1641
|
-
const cliEmptyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: b\n' }), '--json'], { encoding: 'utf8' });
|
|
1642
|
-
assert.equal(cliEmptyBase.status, 2);
|
|
1643
|
-
assert.ok(JSON.parse(cliEmptyBase.stdout).failures.includes('provenance'));
|
|
1644
683
|
});
|
|
1645
684
|
|
|
1646
685
|
test('canonical receipt binds expected provenance when the runtime supplies it', () => {
|
|
1647
686
|
const body = canonicalReceipt();
|
|
1648
|
-
const binding = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
|
|
1649
|
-
assert.deepEqual(binding.expectedProvenance, EXPECTED_PROVENANCE);
|
|
1650
|
-
assert.equal(binding.requireProvenanceBinding, true);
|
|
1651
|
-
assert.equal(binding.runtimeContext.context_id, RUNTIME_CONTEXT.context_id);
|
|
1652
|
-
assert.throws(() => POLICY.createReceiptBinding(EXPECTED_PROVENANCE), /runtime-derived provenance context/);
|
|
1653
687
|
assert.ok(POLICY.validateCanonicalReceipt(body, { requireProvenanceBinding: true }).includes('provenance'));
|
|
1654
688
|
assert.deepEqual(POLICY.validateCanonicalReceipt(body, { expectedProvenance: EXPECTED_PROVENANCE }), []);
|
|
1655
689
|
assert.ok(POLICY.validateCanonicalReceipt(body, { expectedProvenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }).includes('provenance'));
|
|
@@ -1743,37 +777,24 @@ test('runtime provenance derives exact Git evidence, CLI context, and stale/forg
|
|
|
1743
777
|
assert.equal(withIgnoredCache.head, initial.head, 'nested ignored node_modules/cache files must not enter Head');
|
|
1744
778
|
assert.equal(withChangedIgnoredCache.head, initial.head, 'changing nested ignored cache data must not change Head');
|
|
1745
779
|
|
|
1746
|
-
|
|
1747
|
-
const forgedDecision = POLICY.completionDecision('PASS', {
|
|
1748
|
-
workflow_policy: { proof_obligations: ['needsExecutionProof'] },
|
|
1749
|
-
runtime_context: forgedContext,
|
|
1750
|
-
execution_receipt: exactReceipt,
|
|
1751
|
-
});
|
|
1752
|
-
assert.equal(forgedDecision.completion, 'unfinished');
|
|
1753
|
-
assert.deepEqual(forgedDecision.missingProof, ['runtime_provenance']);
|
|
1754
|
-
|
|
1755
|
-
const flash = {
|
|
1756
|
-
status: 'in_progress',
|
|
1757
|
-
receipt: 'FLASH_UNVERIFIED',
|
|
1758
|
-
blocker: 'awaiting test proof',
|
|
1759
|
-
dependencyBlocked: true,
|
|
1760
|
-
unblocks: false,
|
|
1761
|
-
runtime_context: initial,
|
|
1762
|
-
};
|
|
1763
|
-
const promoted = POLICY.promoteFlashTask(flash, 'PASS', exactReceipt);
|
|
1764
|
-
assert.equal(promoted.readyForSync, true);
|
|
1765
|
-
|
|
780
|
+
// A source change after the receipt was written leaves its Base/Head stale.
|
|
1766
781
|
fs.writeFileSync(sourceFile, 'mutated source\n');
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
assert.
|
|
782
|
+
// The pre-change context is recomputed against the current tree, so the old pair no longer binds.
|
|
783
|
+
assert.ok(
|
|
784
|
+
POLICY.validateCanonicalReceipt(exactReceipt, POLICY.receiptValidatorOptions({}, {
|
|
785
|
+
runtimeContext: initial,
|
|
786
|
+
requireProvenanceBinding: true,
|
|
787
|
+
})).includes('provenance'),
|
|
788
|
+
'a receipt bound to the pre-change tree must go stale',
|
|
789
|
+
);
|
|
790
|
+
// A copied context is not runtime-derived and must never supply the expected pair.
|
|
791
|
+
assert.ok(
|
|
792
|
+
POLICY.validateCanonicalReceipt(exactReceipt, POLICY.receiptValidatorOptions({}, {
|
|
793
|
+
runtimeContext: { ...initial },
|
|
794
|
+
requireProvenanceBinding: true,
|
|
795
|
+
})).includes('provenance'),
|
|
796
|
+
'a copied runtime context must not bind',
|
|
797
|
+
);
|
|
1777
798
|
|
|
1778
799
|
fs.writeFileSync(path.join(root, 'src', 'untracked.js'), 'untracked source\n');
|
|
1779
800
|
const withUntracked = PROVENANCE_HELPER.deriveRuntimeContext(input);
|
|
@@ -1867,76 +888,6 @@ test('runtime provenance chooses a deterministic root when all history is Specs-
|
|
|
1867
888
|
}
|
|
1868
889
|
});
|
|
1869
890
|
|
|
1870
|
-
test('lane traces - Direct, Standard, explicit Strict classification, override, state mutation and completion', () => {
|
|
1871
|
-
// Direct: isolated reversible low-risk
|
|
1872
|
-
const direct = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
|
|
1873
|
-
assert.equal(direct.lane, 'Direct');
|
|
1874
|
-
assert.equal(direct.automaticLane, 'Direct');
|
|
1875
|
-
assert.deepEqual(POLICY.lanePolicy(direct).proof_obligations, ['needsExecutionProof']);
|
|
1876
|
-
assert.equal(POLICY.lanePolicy(direct).requiresSpec, false);
|
|
1877
|
-
assert.equal(POLICY.lanePolicy(direct).shipPoint, 'task');
|
|
1878
|
-
// Standard: default
|
|
1879
|
-
const standard = POLICY.classifyLane({ title: 'add pagination to list', taskCount: 1 });
|
|
1880
|
-
assert.equal(standard.lane, 'Standard');
|
|
1881
|
-
assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['needsExecutionProof']);
|
|
1882
|
-
assert.equal(POLICY.lanePolicy(standard).shipPoint, 'feature');
|
|
1883
|
-
// Risk alone is Elevated; Critical requires an explicit Strict assurance choice.
|
|
1884
|
-
const elevated = POLICY.classifyLane({ riskSignals: { auth: true }, taskCount: 1 });
|
|
1885
|
-
assert.equal(elevated.lane, 'Standard');
|
|
1886
|
-
assert.deepEqual(POLICY.lanePolicy(elevated).proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
1887
|
-
const critical = POLICY.classifyLane({ riskSignals: { auth: true }, assurance_level: 'Strict', taskCount: 1 });
|
|
1888
|
-
assert.equal(critical.lane, 'Critical');
|
|
1889
|
-
assert.ok(critical.risks.includes('auth'));
|
|
1890
|
-
assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
1891
|
-
// Override: Direct requesting Critical is allowed (upgrade) without extra auth
|
|
1892
|
-
const upgrade = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1, override: 'Critical' });
|
|
1893
|
-
assert.equal(upgrade.lane, 'Critical');
|
|
1894
|
-
assert.equal(upgrade.automaticLane, 'Direct');
|
|
1895
|
-
// Downgrade without auth is blocked
|
|
1896
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /assurance_level downgrade.*blocked/i);
|
|
1897
|
-
// Caller-owned booleans cannot weaken the classified minimum.
|
|
1898
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true }), /assurance_level downgrade.*blocked/i);
|
|
1899
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', user_approved: true }), /assurance_level downgrade.*blocked/i);
|
|
1900
|
-
// State mutation: approvalState
|
|
1901
|
-
const pendingApproval = POLICY.approvalState({ generated: true, agent_validated: false, user_approved: false });
|
|
1902
|
-
assert.equal(pendingApproval.ready, false);
|
|
1903
|
-
const agentValidated = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false });
|
|
1904
|
-
assert.equal(agentValidated.ready, true, 'generated plus agent validation is technical readiness');
|
|
1905
|
-
const approved = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true });
|
|
1906
|
-
assert.equal(approved.ready, true);
|
|
1907
|
-
// Completion: flash work remains in_progress and does not unblock
|
|
1908
|
-
const flashTask = canonicalFlashTask();
|
|
1909
|
-
const failRemains = POLICY.promoteFlashTask(flashTask, 'FAIL');
|
|
1910
|
-
assert.equal(failRemains.status, 'in_progress');
|
|
1911
|
-
assert.equal(failRemains.unblocks, false);
|
|
1912
|
-
const blockedRemains = POLICY.promoteFlashTask(flashTask, 'BLOCKED');
|
|
1913
|
-
assert.equal(blockedRemains.unblocks, false);
|
|
1914
|
-
const noTestsRemains = POLICY.promoteFlashTask(flashTask, 'NO_TESTS');
|
|
1915
|
-
assert.equal(noTestsRemains.unblocks, false);
|
|
1916
|
-
const promoted = POLICY.promoteFlashTask(flashTask, 'PASS', canonicalReceipt());
|
|
1917
|
-
assert.equal(promoted.readyForSync, true);
|
|
1918
|
-
assert.equal(promoted.unblocks, false, 'promoted flash must not unblock until sync-finalize');
|
|
1919
|
-
const finalized = POLICY.syncFinalizeFlashTask(flashTask, 'PASS', promoted.receipt);
|
|
1920
|
-
assert.equal(finalized.status, 'done');
|
|
1921
|
-
assert.equal(finalized.unblocks, true);
|
|
1922
|
-
});
|
|
1923
|
-
|
|
1924
|
-
test('flash selective promotion - only specific task is promoted, not blanket', () => {
|
|
1925
|
-
const registry = {
|
|
1926
|
-
'tasks/task-a.md': canonicalFlashTask(),
|
|
1927
|
-
'tasks/task-b.md': canonicalFlashTask(),
|
|
1928
|
-
};
|
|
1929
|
-
const promotedA = POLICY.promoteFlashTask(registry['tasks/task-a.md'], 'PASS', canonicalReceipt());
|
|
1930
|
-
const notPromotedB = registry['tasks/task-b.md']; // unchanged
|
|
1931
|
-
assert.equal(promotedA.readyForSync, true);
|
|
1932
|
-
assert.equal(notPromotedB.receipt, 'FLASH_UNVERIFIED');
|
|
1933
|
-
assert.equal(notPromotedB.readyForSync, undefined);
|
|
1934
|
-
// FAIL on B does not affect A
|
|
1935
|
-
const failB = POLICY.promoteFlashTask(notPromotedB, 'FAIL');
|
|
1936
|
-
assert.equal(failB.receipt, 'FLASH_UNVERIFIED');
|
|
1937
|
-
assert.equal(promotedA.receipt.startsWith('Verification: PASS'), true);
|
|
1938
|
-
});
|
|
1939
|
-
|
|
1940
891
|
test('parallel waves require immutable provenance receipts and safe recovery', () => {
|
|
1941
892
|
const waves = markdownLegacyRegions(read(PARALLEL_WAVES)).primary;
|
|
1942
893
|
assert.match(waves, /only for explicit parallel execution/i);
|
|
@@ -2644,36 +1595,6 @@ test('P0 receipt fail-closed: Exit 1/-1/abc/conflict/empty command rejected', ()
|
|
|
2644
1595
|
assert.deepEqual(fails, [], `case ${desc} should pass, got ${fails}`);
|
|
2645
1596
|
}
|
|
2646
1597
|
}
|
|
2647
|
-
// CLI parity: Exit 1 must be rejected via CLI
|
|
2648
|
-
const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: `${base}Exit: 1\n` }), '--json'], { encoding: 'utf8' });
|
|
2649
|
-
assert.equal(cli.status, 2);
|
|
2650
|
-
assert.equal(JSON.parse(cli.stdout).ok, false);
|
|
2651
|
-
});
|
|
2652
|
-
|
|
2653
|
-
test('P0 flash marker-only must not promote or finalize', () => {
|
|
2654
|
-
const flash = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
|
|
2655
|
-
const markerOnly = 'Verification: PASS';
|
|
2656
|
-
const promoted = POLICY.promoteFlashTask(flash, 'PASS', markerOnly);
|
|
2657
|
-
assert.equal(promoted.status, 'in_progress');
|
|
2658
|
-
assert.equal(promoted.receipt, 'FLASH_UNVERIFIED');
|
|
2659
|
-
assert.equal(promoted.readyForSync, false);
|
|
2660
|
-
assert.equal(promoted.dependencyBlocked, true);
|
|
2661
|
-
assert.equal(promoted.unblocks, false);
|
|
2662
|
-
assert.match(promoted.blocker, /canonical receipt/);
|
|
2663
|
-
// Even with PASS marker but missing command/exit/provenance, finalize must not complete
|
|
2664
|
-
const fakePromoted = { status: 'in_progress', receipt: markerOnly, readyForSync: true, dependencyBlocked: true, unblocks: false };
|
|
2665
|
-
const notFinalized = POLICY.finalizeFlashTask(fakePromoted, 'sync-finalize');
|
|
2666
|
-
assert.equal(notFinalized.status, 'in_progress', 'marker-only receipt must not finalize');
|
|
2667
|
-
// Valid receipt promotes correctly
|
|
2668
|
-
const valid = canonicalReceipt();
|
|
2669
|
-
const good = POLICY.promoteFlashTask(flash, 'PASS', valid);
|
|
2670
|
-
assert.equal(good.readyForSync, true);
|
|
2671
|
-
assert.equal(good.receipt, valid.trim());
|
|
2672
|
-
const forgedFinalize = POLICY.finalizeFlashTask(good, 'sync-finalize');
|
|
2673
|
-
assert.equal(forgedFinalize.status, 'in_progress');
|
|
2674
|
-
assert.equal(forgedFinalize.unblocks, false);
|
|
2675
|
-
const finalized = POLICY.syncFinalizeFlashTask(flash, 'PASS', valid);
|
|
2676
|
-
assert.equal(finalized.status, 'done');
|
|
2677
1598
|
});
|
|
2678
1599
|
|
|
2679
1600
|
test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact receipts compatible', () => {
|
|
@@ -2697,123 +1618,12 @@ test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact
|
|
|
2697
1618
|
}
|
|
2698
1619
|
});
|
|
2699
1620
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
dependencyBlocked: true,
|
|
2707
|
-
unblocks: false,
|
|
2708
|
-
flashTransition: 'promoted',
|
|
2709
|
-
promotionReceipt: receipt,
|
|
2710
|
-
};
|
|
2711
|
-
const direct = spawnSync(process.execPath, [
|
|
2712
|
-
POLICY_PATH,
|
|
2713
|
-
'--sync-finalize',
|
|
2714
|
-
'--task-json',
|
|
2715
|
-
JSON.stringify(forged),
|
|
2716
|
-
'--json',
|
|
2717
|
-
], { encoding: 'utf8' });
|
|
2718
|
-
assert.equal(direct.status, 2, `${direct.stdout}\n${direct.stderr}`);
|
|
2719
|
-
const directPayload = JSON.parse(direct.stdout);
|
|
2720
|
-
assert.equal(directPayload.ok, false);
|
|
2721
|
-
assert.equal(directPayload.task.status, 'in_progress');
|
|
2722
|
-
assert.equal(directPayload.task.receipt, 'FLASH_UNVERIFIED');
|
|
2723
|
-
assert.equal(directPayload.task.readyForSync, false);
|
|
2724
|
-
assert.equal(directPayload.task.dependencyBlocked, true);
|
|
2725
|
-
assert.equal(directPayload.task.unblocks, false);
|
|
2726
|
-
assert.match(directPayload.message, /current task must be FLASH_UNVERIFIED.*canonical/i);
|
|
2727
|
-
|
|
2728
|
-
const minimal = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED' };
|
|
2729
|
-
const minimalResult = POLICY.syncFinalizeFlashTask(minimal, 'PASS', receipt);
|
|
2730
|
-
assert.equal(minimalResult.status, 'in_progress');
|
|
2731
|
-
assert.match(minimalResult.blocker, /exact canonical stored state/);
|
|
2732
|
-
const forgedOpen = { ...canonicalFlashTask(), readyForSync: true };
|
|
2733
|
-
const forgedOpenResult = POLICY.syncFinalizeFlashTask(forgedOpen, 'PASS', receipt);
|
|
2734
|
-
assert.equal(forgedOpenResult.status, 'in_progress');
|
|
2735
|
-
assert.equal(forgedOpenResult.unblocks, false);
|
|
2736
|
-
|
|
2737
|
-
const current = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
|
|
2738
|
-
const supported = spawnSync(process.execPath, [
|
|
2739
|
-
POLICY_PATH,
|
|
2740
|
-
'--sync-finalize',
|
|
2741
|
-
'--task-json',
|
|
2742
|
-
JSON.stringify(current),
|
|
2743
|
-
'--verdict',
|
|
2744
|
-
'PASS',
|
|
2745
|
-
'--proof',
|
|
2746
|
-
receipt,
|
|
2747
|
-
'--project-root',
|
|
2748
|
-
RUNTIME_ROOT,
|
|
2749
|
-
'--specs-root',
|
|
2750
|
-
path.join(RUNTIME_ROOT, 'specs'),
|
|
2751
|
-
'--spec-file',
|
|
2752
|
-
RUNTIME_SPEC,
|
|
2753
|
-
'--feature-name',
|
|
2754
|
-
'demo',
|
|
2755
|
-
'--runtime-session',
|
|
2756
|
-
'implementation-session-1',
|
|
2757
|
-
'--json',
|
|
2758
|
-
], { encoding: 'utf8' });
|
|
2759
|
-
assert.equal(supported.status, 0, `${supported.stdout}\n${supported.stderr}`);
|
|
2760
|
-
assert.equal(JSON.parse(supported.stdout).task.status, 'done');
|
|
2761
|
-
const warningFinalize = POLICY.syncFinalizeFlashTask(current, 'PASS_WITH_WARNINGS', receipt);
|
|
2762
|
-
assert.equal(warningFinalize.status, 'in_progress');
|
|
2763
|
-
assert.equal(warningFinalize.unblocks, false);
|
|
2764
|
-
|
|
2765
|
-
const initial = canonicalFlashTask();
|
|
2766
|
-
const promoted = POLICY.promoteFlashTask(initial, 'PASS', receipt);
|
|
2767
|
-
const persisted = JSON.parse(JSON.stringify(promoted));
|
|
2768
|
-
assert.equal(persisted.flashTransition, 'promoted');
|
|
2769
|
-
assert.equal(persisted.promotionReceipt, receipt.trim());
|
|
2770
|
-
const forgedPersistedFinalize = POLICY.finalizeFlashTask(persisted, 'sync-finalize');
|
|
2771
|
-
assert.equal(forgedPersistedFinalize.status, 'in_progress');
|
|
2772
|
-
assert.equal(forgedPersistedFinalize.unblocks, false);
|
|
2773
|
-
const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', receipt);
|
|
2774
|
-
assert.equal(finalized.status, 'done');
|
|
2775
|
-
assert.equal(finalized.flashTransition, 'finalized');
|
|
2776
|
-
|
|
2777
|
-
const artifactInitial = { ...initial, artifacts: ['output/bundle.js'] };
|
|
2778
|
-
const artifactPromotion = POLICY.promoteFlashTask(artifactInitial, 'PASS', receipt);
|
|
2779
|
-
assert.equal(artifactPromotion.readyForSync, false, 'flash promotion must apply task artifact requirements');
|
|
2780
|
-
|
|
2781
|
-
const badProof = spawnSync(process.execPath, [
|
|
2782
|
-
POLICY_PATH,
|
|
2783
|
-
'--sync-finalize',
|
|
2784
|
-
'--task-json',
|
|
2785
|
-
JSON.stringify(current),
|
|
2786
|
-
'--verdict',
|
|
2787
|
-
'PASS',
|
|
2788
|
-
'--proof',
|
|
2789
|
-
'Verification: PASS',
|
|
2790
|
-
'--json',
|
|
2791
|
-
], { encoding: 'utf8' });
|
|
2792
|
-
assert.equal(badProof.status, 2);
|
|
2793
|
-
assert.equal(JSON.parse(badProof.stdout).task.status, 'in_progress');
|
|
2794
|
-
|
|
2795
|
-
const badArtifact = spawnSync(process.execPath, [
|
|
2796
|
-
POLICY_PATH,
|
|
2797
|
-
'--sync-finalize',
|
|
2798
|
-
'--task-json',
|
|
2799
|
-
JSON.stringify({ ...current, artifacts: ['output/bundle.js'] }),
|
|
2800
|
-
'--verdict',
|
|
2801
|
-
'PASS',
|
|
2802
|
-
'--proof',
|
|
2803
|
-
receipt,
|
|
2804
|
-
'--json',
|
|
2805
|
-
], { encoding: 'utf8' });
|
|
2806
|
-
assert.equal(badArtifact.status, 2);
|
|
2807
|
-
assert.equal(JSON.parse(badArtifact.stdout).task.status, 'in_progress');
|
|
2808
|
-
|
|
2809
|
-
const markerOnly = {
|
|
2810
|
-
...forged,
|
|
2811
|
-
flashTransition: 'promoted',
|
|
2812
|
-
promotionReceipt: 'Verification: PASS',
|
|
2813
|
-
receipt: 'Verification: PASS',
|
|
2814
|
-
};
|
|
2815
|
-
assert.equal(POLICY.finalizeFlashTask(markerOnly, 'sync-finalize').status, 'in_progress');
|
|
2816
|
-
});
|
|
1621
|
+
function writeWorkflowPacketAt(featureDir, taskStatus = 'pending', dependency = 'none') {
|
|
1622
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
1623
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
1624
|
+
fs.writeFileSync(path.join(featureDir, 'task-01-x.md'),
|
|
1625
|
+
`# Task 01\n\nStatus: ${taskStatus}\n\n## Dependencies\n\n- ${dependency}\n\n## Verification Plan\n\n- Command: pnpm test\n`);
|
|
1626
|
+
}
|
|
2817
1627
|
|
|
2818
1628
|
test('P0 Claude and Codex reject configured specs roots outside the project', () => {
|
|
2819
1629
|
const claudeResolver = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
|
|
@@ -2821,15 +1631,11 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
|
|
|
2821
1631
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-external-specs-'));
|
|
2822
1632
|
const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-configured-specs-'));
|
|
2823
1633
|
try {
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
fs.mkdirSync(local, { recursive: true });
|
|
2827
|
-
fs.mkdirSync(external, { recursive: true });
|
|
2828
|
-
fs.writeFileSync(path.join(local, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'local' }));
|
|
2829
|
-
fs.writeFileSync(path.join(external, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'remote' }));
|
|
1634
|
+
writeWorkflowPacketAt(path.join(tmp, 'specs', 'local'));
|
|
1635
|
+
writeWorkflowPacketAt(path.join(externalRoot, 'remote'));
|
|
2830
1636
|
const runtime = { paths: { specs: externalRoot } };
|
|
2831
|
-
const claude = claudeResolver.
|
|
2832
|
-
const codex = codexUtils.
|
|
1637
|
+
const claude = claudeResolver.resolveWorkflowCandidate({ projectRoot: tmp, runtime });
|
|
1638
|
+
const codex = codexUtils.resolveWorkflowCandidate(tmp, runtime, null, null);
|
|
2833
1639
|
assert.equal(claude.error, 'invalid_specs');
|
|
2834
1640
|
assert.equal(codex.error, 'invalid_specs');
|
|
2835
1641
|
assert.match(claude.reason, /escapes project root/);
|
|
@@ -2840,59 +1646,31 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
|
|
|
2840
1646
|
}
|
|
2841
1647
|
});
|
|
2842
1648
|
|
|
2843
|
-
test('P0 caller-owned downgrade flags have no canonical capability', () => {
|
|
2844
|
-
for (const callerClaim of [{ user_approved: true }, { userApproved: true }, { userAuthorized: true }]) {
|
|
2845
|
-
assert.throws(
|
|
2846
|
-
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct', ...callerClaim }),
|
|
2847
|
-
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2848
|
-
);
|
|
2849
|
-
}
|
|
2850
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { auth: true }, override: 'Standard', confirmDowngrade: true }), /assurance_level downgrade.*blocked/i);
|
|
2851
|
-
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
2852
|
-
});
|
|
2853
|
-
|
|
2854
|
-
test('P0 canonical policy exposes no caller-owned downgrade authority', () => {
|
|
2855
|
-
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
2856
|
-
assert.equal(POLICY.isValidOverrideReceipt, undefined);
|
|
2857
|
-
assert.equal(POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS.includes('override_' + 'receipt'), false);
|
|
2858
|
-
assert.throws(
|
|
2859
|
-
() => POLICY.classifyLane({
|
|
2860
|
-
riskSignals: { privacy: true },
|
|
2861
|
-
override: 'Direct',
|
|
2862
|
-
overrideReceipt: { verifiedByRuntime: true },
|
|
2863
|
-
}),
|
|
2864
|
-
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2865
|
-
);
|
|
2866
|
-
});
|
|
2867
|
-
|
|
2868
1649
|
test('P0 active spec deterministic: multiple active ambiguity and explicit target/path containment', () => {
|
|
2869
1650
|
const RESOLVER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
|
|
2870
1651
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-'));
|
|
2871
1652
|
try {
|
|
2872
1653
|
const specsDir = path.join(tmp, 'specs');
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
for (const name of ['alpha', 'beta']) {
|
|
2876
|
-
fs.mkdirSync(path.join(specsDir, name), { recursive: true });
|
|
2877
|
-
fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name, current_phase: 'design' }));
|
|
2878
|
-
}
|
|
2879
|
-
const amb = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
|
|
1654
|
+
for (const name of ['alpha', 'beta']) writeWorkflowPacketAt(path.join(specsDir, name));
|
|
1655
|
+
const amb = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
2880
1656
|
assert.equal(amb.error, 'multiple_active');
|
|
2881
1657
|
assert.deepEqual(amb.candidates.sort(), ['alpha', 'beta']);
|
|
2882
1658
|
// Explicit feature resolves deterministically
|
|
2883
|
-
const alpha = RESOLVER.
|
|
1659
|
+
const alpha = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'alpha' });
|
|
2884
1660
|
assert.equal(alpha.featureName, 'alpha');
|
|
2885
1661
|
// Explicit not-found fail-closed
|
|
2886
|
-
const notFound = RESOLVER.
|
|
1662
|
+
const notFound = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'gamma' });
|
|
2887
1663
|
assert.equal(notFound.error, 'explicit_not_found');
|
|
2888
|
-
// Explicit path containment:
|
|
2889
|
-
|
|
1664
|
+
// Explicit path containment: a plan.md reached through ../ is outside the specs root
|
|
1665
|
+
fs.mkdirSync(path.join(tmp, 'etc'), { recursive: true });
|
|
1666
|
+
fs.writeFileSync(path.join(tmp, 'etc', 'plan.md'), '# Outside\n');
|
|
1667
|
+
const escape = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(tmp, 'specs', '..', 'etc', 'plan.md') });
|
|
2890
1668
|
assert.equal(escape.error, 'explicit_malformed');
|
|
2891
1669
|
// Malformed feature name with slash must be rejected
|
|
2892
|
-
const malformed = RESOLVER.
|
|
1670
|
+
const malformed = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: '../alpha' });
|
|
2893
1671
|
assert.equal(malformed.error, 'explicit_malformed');
|
|
2894
1672
|
// Valid explicit path inside root
|
|
2895
|
-
const validPath = RESOLVER.
|
|
1673
|
+
const validPath = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'beta', 'plan.md') });
|
|
2896
1674
|
assert.equal(validPath.featureName, 'beta');
|
|
2897
1675
|
} finally {
|
|
2898
1676
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
@@ -2911,30 +1689,23 @@ test('P0 parity Claude/Codex: canonical receipt and multi-active', () => {
|
|
|
2911
1689
|
const codexUtils = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
|
|
2912
1690
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-resolver-'));
|
|
2913
1691
|
try {
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
for (const name of ['x', 'y']) {
|
|
2917
|
-
fs.mkdirSync(path.join(specsDir, name), { recursive: true });
|
|
2918
|
-
fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name }));
|
|
2919
|
-
}
|
|
2920
|
-
const amb = codexUtils.resolveActiveSpec(tmp, {}, null, null);
|
|
1692
|
+
for (const name of ['x', 'y']) writeWorkflowPacketAt(path.join(tmp, 'specs', name));
|
|
1693
|
+
const amb = codexUtils.resolveWorkflowCandidate(tmp, {}, null, null);
|
|
2921
1694
|
assert.equal(amb.error, 'multiple_active');
|
|
2922
|
-
|
|
1695
|
+
fs.mkdirSync(path.join(tmp, 'outside'), { recursive: true });
|
|
1696
|
+
fs.writeFileSync(path.join(tmp, 'outside', 'plan.md'), '# Outside\n');
|
|
1697
|
+
const esc = codexUtils.resolveWorkflowCandidate(tmp, {}, null, path.join(tmp, 'specs', '..', 'outside', 'plan.md'));
|
|
2923
1698
|
assert.equal(esc.error, 'explicit_malformed');
|
|
2924
1699
|
} finally {
|
|
2925
1700
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
2926
1701
|
}
|
|
2927
1702
|
});
|
|
2928
1703
|
|
|
2929
|
-
test('P0 regression: placeholder, explicit failure
|
|
2930
|
-
// Probe A: explicit failure outcomes must fail canonical validator
|
|
1704
|
+
test('P0 regression: placeholder, explicit failure and artifact receipts are blocked', () => {
|
|
1705
|
+
// Probe A: explicit failure outcomes must fail the canonical validator
|
|
2931
1706
|
const bodyTestsFailed = 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nTests failed: 1\n';
|
|
2932
1707
|
assert.ok(POLICY.validateCanonicalReceipt(bodyTestsFailed).length > 0, 'Tests failed: 1 must be rejected');
|
|
2933
1708
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nResult: FAIL\nResult: PASS\n').includes('exit_result'), 'Result FAIL then PASS must be rejected');
|
|
2934
|
-
const flashTask = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED', blocker: 'awaiting', dependencyBlocked: true, unblocks: false };
|
|
2935
|
-
const notPromoted = POLICY.promoteFlashTask(flashTask, 'PASS', bodyTestsFailed);
|
|
2936
|
-
assert.equal(notPromoted.readyForSync, false, 'flash with Tests failed should not promote');
|
|
2937
|
-
assert.equal(notPromoted.receipt, 'FLASH_UNVERIFIED');
|
|
2938
1709
|
|
|
2939
1710
|
// Probe B: placeholder tokens must be rejected, but substring todo must pass
|
|
2940
1711
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: TODO\nExit: 0\nBase: a\nHead: b\n').includes('command'), 'Command TODO must be rejected');
|
|
@@ -2949,24 +1720,6 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
|
|
|
2949
1720
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: bundle\nsha256: abc123\n').includes('artifact_hash'), 'short artifact sha must fail');
|
|
2950
1721
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact produced sha256:deadbeef\n').includes('artifact_hash'), 'short inline sha256 must fail');
|
|
2951
1722
|
assert.deepEqual(POLICY.validateCanonicalReceipt(`Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\nArtifact: bundle\nsha256: ${'c'.repeat(64)}\n`), [], '64-hex artifact sha should pass');
|
|
2952
|
-
|
|
2953
|
-
// Probe F: lanePolicy forged plain object must not bypass classification
|
|
2954
|
-
const { spawnSync } = require('node:child_process');
|
|
2955
|
-
const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
|
|
2956
|
-
const forged = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct', riskSignals: { auth: true } }), '--json'], { encoding: 'utf8' });
|
|
2957
|
-
assert.equal(forged.status, 2, 'forged Direct with auth should be blocked via lanePolicy');
|
|
2958
|
-
assert.match(forged.stderr, /downgrade.*not permitted/i);
|
|
2959
|
-
const forged2 = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct' }), '--json'], { encoding: 'utf8' });
|
|
2960
|
-
assert.equal(forged2.status, 2, 'plain Direct without justification should be blocked');
|
|
2961
|
-
|
|
2962
|
-
const trusted = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true });
|
|
2963
|
-
assert.equal(trusted.lane, 'Direct');
|
|
2964
|
-
const viaPolicy = POLICY.lanePolicy(trusted);
|
|
2965
|
-
assert.equal(viaPolicy.lane, 'Direct', 'trusted Direct should be respected');
|
|
2966
|
-
assert.throws(() => { 'use strict'; trusted.lane = 'Direct'; }, /read only|Cannot assign|TypeError/);
|
|
2967
|
-
const paymentTrusted = POLICY.classifyLane({ riskSignals: { payment: true } });
|
|
2968
|
-
const forgedCopy = { ...paymentTrusted, lane: 'Direct' };
|
|
2969
|
-
assert.equal(POLICY.lanePolicy(forgedCopy).lane, 'Standard', 'compatibility lane is derived from authoritative axes');
|
|
2970
1723
|
});
|
|
2971
1724
|
|
|
2972
1725
|
test('P0 regression: symlink spec/task containment and malformed spec handling', () => {
|
|
@@ -2974,33 +1727,29 @@ test('P0 regression: symlink spec/task containment and malformed spec handling',
|
|
|
2974
1727
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-regression-symlink-'));
|
|
2975
1728
|
try {
|
|
2976
1729
|
const specsDir = path.join(tmp, 'specs');
|
|
2977
|
-
|
|
2978
|
-
fs.mkdirSync(path.join(specsDir, 'valid'), { recursive: true });
|
|
2979
|
-
fs.writeFileSync(path.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'valid' }));
|
|
1730
|
+
writeWorkflowPacketAt(path.join(specsDir, 'valid'));
|
|
2980
1731
|
const outside = path.join(tmp, 'outside');
|
|
2981
|
-
|
|
2982
|
-
fs.
|
|
2983
|
-
const
|
|
2984
|
-
fs.symlinkSync(outside, link);
|
|
2985
|
-
const e1 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'linked' });
|
|
1732
|
+
writeWorkflowPacketAt(outside);
|
|
1733
|
+
fs.symlinkSync(outside, path.join(specsDir, 'linked'));
|
|
1734
|
+
const e1 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'linked' });
|
|
2986
1735
|
assert.equal(e1.error, 'explicit_malformed');
|
|
2987
|
-
const e2 = RESOLVER.
|
|
1736
|
+
const e2 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'linked', 'plan.md') });
|
|
2988
1737
|
assert.equal(e2.error, 'explicit_malformed');
|
|
2989
|
-
const nonExplicit = RESOLVER.
|
|
1738
|
+
const nonExplicit = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
2990
1739
|
assert.equal(nonExplicit.error, 'invalid_specs');
|
|
2991
1740
|
assert.ok(nonExplicit.candidates.includes('linked'));
|
|
2992
|
-
|
|
2993
|
-
|
|
1741
|
+
fs.rmSync(path.join(specsDir, 'linked'));
|
|
1742
|
+
// A task file that is a symlink to a file outside the packet is refused.
|
|
2994
1743
|
const taskOutside = path.join(tmp, 'outside-task.md');
|
|
2995
|
-
fs.
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
const mal = RESOLVER.
|
|
1744
|
+
fs.copyFileSync(path.join(specsDir, 'valid', 'task-01-x.md'), taskOutside);
|
|
1745
|
+
fs.symlinkSync(taskOutside, path.join(specsDir, 'valid', 'task-02-link.md'));
|
|
1746
|
+
const linkedTask = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
1747
|
+
assert.equal(linkedTask.error, 'invalid_specs');
|
|
1748
|
+
assert.ok(linkedTask.candidates.includes('valid'));
|
|
1749
|
+
fs.rmSync(path.join(specsDir, 'valid', 'task-02-link.md'));
|
|
1750
|
+
// A malformed packet (dependency on a missing task) is reported, not skipped.
|
|
1751
|
+
writeWorkflowPacketAt(path.join(specsDir, 'bad'), 'pending', 'task-09-missing.md');
|
|
1752
|
+
const mal = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3004
1753
|
assert.equal(mal.error, 'invalid_specs');
|
|
3005
1754
|
assert.ok(mal.candidates.includes('bad'));
|
|
3006
1755
|
} finally {
|
|
@@ -3071,18 +1820,18 @@ test('P0 regression: safeTaskFile fail-closed on realpath error and single autho
|
|
|
3071
1820
|
assert.match(codexSrc, /getSharedValidate/);
|
|
3072
1821
|
assert.match(codexSrc, /workflow-policy\.cjs/);
|
|
3073
1822
|
assert.doesNotMatch(codexSrc, /const PLACEHOLDER_TOKENS/);
|
|
3074
|
-
//
|
|
1823
|
+
// The task-file read fails closed when realpath throws
|
|
3075
1824
|
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-realpath-'));
|
|
3076
1825
|
try {
|
|
3077
1826
|
const featDir = path2.join(tmp, 'feat');
|
|
3078
|
-
|
|
3079
|
-
fs2.
|
|
1827
|
+
writeWorkflowPacketAt(featDir, 'done');
|
|
1828
|
+
fs2.appendFileSync(path2.join(featDir, 'task-01-x.md'), '\n## Receipt\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n```\npnpm test\n```\n');
|
|
1829
|
+
const codexReceipt = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'));
|
|
3080
1830
|
const orig = fs2.realpathSync;
|
|
3081
1831
|
fs2.realpathSync = () => { throw new Error('simulated realpath failure'); };
|
|
3082
1832
|
try {
|
|
3083
|
-
const
|
|
3084
|
-
|
|
3085
|
-
assert.ok(fails.includes('a'), 'realpath failure should be fail-closed with check a');
|
|
1833
|
+
const proof = codexReceipt.checkWorkflowReceiptDetails(featDir, 'task-01-x.md', null);
|
|
1834
|
+
assert.ok(proof.failures.includes('unsafe_path'), `realpath failure must fail closed, got ${proof.failures}`);
|
|
3086
1835
|
} finally {
|
|
3087
1836
|
fs2.realpathSync = orig;
|
|
3088
1837
|
}
|
|
@@ -3099,15 +1848,12 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3099
1848
|
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-resolver-fail-'));
|
|
3100
1849
|
try {
|
|
3101
1850
|
const specsDir = path2.join(tmp, 'specs');
|
|
3102
|
-
|
|
3103
|
-
fs2.writeFileSync(path2.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo' }));
|
|
3104
|
-
// Monkey patch realpathSync to always throw for explicitFeature
|
|
1851
|
+
writeWorkflowPacketAt(path2.join(specsDir, 'demo'));
|
|
3105
1852
|
const origRealpath = fs2.realpathSync;
|
|
3106
1853
|
fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
|
|
3107
1854
|
try {
|
|
3108
|
-
const res = RESOLVER.
|
|
1855
|
+
const res = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'demo' });
|
|
3109
1856
|
assert.equal(res.error, 'explicit_malformed', 'explicitFeature realpath failure should be explicit_malformed');
|
|
3110
|
-
assert.match(res.reason, /canonicalization error/);
|
|
3111
1857
|
} finally {
|
|
3112
1858
|
fs2.realpathSync = origRealpath;
|
|
3113
1859
|
}
|
|
@@ -3118,7 +1864,7 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3118
1864
|
return origRealpath2(p);
|
|
3119
1865
|
};
|
|
3120
1866
|
try {
|
|
3121
|
-
const res2 = RESOLVER.
|
|
1867
|
+
const res2 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3122
1868
|
assert.equal(res2.error, 'invalid_specs');
|
|
3123
1869
|
assert.ok(res2.candidates.includes('<specs>'));
|
|
3124
1870
|
} finally {
|
|
@@ -3131,21 +1877,17 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3131
1877
|
return origLstat(p);
|
|
3132
1878
|
};
|
|
3133
1879
|
try {
|
|
3134
|
-
const res3 = RESOLVER.
|
|
1880
|
+
const res3 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3135
1881
|
assert.equal(res3.error, 'invalid_specs');
|
|
3136
1882
|
assert.ok(res3.candidates.includes('demo'));
|
|
3137
1883
|
} finally {
|
|
3138
1884
|
fs2.lstatSync = origLstat;
|
|
3139
1885
|
}
|
|
3140
|
-
//
|
|
3141
|
-
const outside = path2.join(tmp, 'outside-spec2');
|
|
3142
|
-
fs2.mkdirSync(outside, { recursive: true });
|
|
3143
|
-
fs2.writeFileSync(path2.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
|
|
1886
|
+
// A dangling plan.md symlink is invalid
|
|
3144
1887
|
const featDir = path2.join(specsDir, 'dangle');
|
|
3145
1888
|
fs2.mkdirSync(featDir, { recursive: true });
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
const res4 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
|
|
1889
|
+
fs2.symlinkSync(path2.join(tmp, 'nonexistent-target'), path2.join(featDir, 'plan.md'));
|
|
1890
|
+
const res4 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3149
1891
|
assert.equal(res4.error, 'invalid_specs');
|
|
3150
1892
|
assert.ok(res4.candidates.includes('dangle'));
|
|
3151
1893
|
} finally {
|
|
@@ -3155,13 +1897,11 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3155
1897
|
const codexUtils = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
|
|
3156
1898
|
const tmp2 = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-fail-'));
|
|
3157
1899
|
try {
|
|
3158
|
-
|
|
3159
|
-
fs2.mkdirSync(path2.join(specsDir2, 'demo2'), { recursive: true });
|
|
3160
|
-
fs2.writeFileSync(path2.join(specsDir2, 'demo2', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo2' }));
|
|
1900
|
+
writeWorkflowPacketAt(path2.join(tmp2, 'specs', 'demo2'));
|
|
3161
1901
|
const orig = fs2.realpathSync;
|
|
3162
1902
|
fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
|
|
3163
1903
|
try {
|
|
3164
|
-
const r = codexUtils.
|
|
1904
|
+
const r = codexUtils.resolveWorkflowCandidate(tmp2, {}, 'demo2', null);
|
|
3165
1905
|
assert.equal(r.error, 'explicit_malformed');
|
|
3166
1906
|
} finally {
|
|
3167
1907
|
fs2.realpathSync = orig;
|