@haposoft/cafekit 0.16.0-rc.4 → 0.16.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +136 -64
- package/bin/__tests__/develop-contract.test.js +55 -17
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/package-inventory.test.js +29 -11
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +586 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +59 -12
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +78 -14
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +222 -42
- package/bin/__tests__/validator-grounding.test.js +103 -4
- package/package.json +3 -2
- package/src/claude/hooks/__tests__/completion-authority.test.js +31 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +111 -29
- package/src/claude/hooks/privacy-block.cjs +38 -14
- package/src/claude/scripts/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +46 -0
- package/src/claude/scripts/spec-ground.cjs +46 -10
- package/src/claude/scripts/spec-readiness.cjs +324 -17
- package/src/claude/scripts/spec-scaffold.cjs +268 -28
- package/src/claude/scripts/validate-spec-output.cjs +262 -10
- package/src/claude/skills/specs/references/review.md +14 -2
- package/src/claude/skills/specs/templates/spec-state.json +14 -2
- package/src/codex/hooks/lib/privacy-state.cjs +23 -22
- package/src/codex/hooks/privacy-approval.cjs +7 -3
- package/src/codex/hooks/privacy-block.cjs +45 -22
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// R0-01 owned semantic-firewall test for the D1 production CLI callers
|
|
4
|
+
// wired around change-firewall.cjs: release-preflight.mjs (verify-only) and
|
|
5
|
+
// benchmark-workflow.mjs's `freeze` verb (create-only). Every scenario below
|
|
6
|
+
// runs against an isolated temp checkout this file constructs by copying the
|
|
7
|
+
// real, unmodified source scripts into a fresh git repo -- change-firewall.cjs
|
|
8
|
+
// resolves its production root via git from its own file location, so a
|
|
9
|
+
// faithful copy under the same relative layout is what actually lets these
|
|
10
|
+
// CLIs run against a fixture instead of this repo's own real authority state.
|
|
11
|
+
// The real checkout is only ever read (package.json version/scripts), never mutated.
|
|
12
|
+
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { spawnSync } = require('node:child_process');
|
|
18
|
+
const { test } = require('node:test');
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
21
|
+
const cf = require(path.join(ROOT, 'src/claude/scripts/change-firewall.cjs'));
|
|
22
|
+
const DIGEST = require(path.join(ROOT, 'src/claude/scripts/spec-authoring-digest.cjs'));
|
|
23
|
+
const VALIDATOR = require(path.join(ROOT, 'src/claude/scripts/validate-spec-output.cjs'));
|
|
24
|
+
const POLICY = require(path.join(ROOT, 'src/claude/scripts/workflow-policy.cjs'));
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// generic fixture helpers (self-contained; mirrors change-firewall.semantic-firewall.test.js)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function git(root, args) {
|
|
31
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
32
|
+
assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stdout}\n${result.stderr}`);
|
|
33
|
+
return result.stdout;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeJson(file, value) {
|
|
37
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
38
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeFile(file, content) {
|
|
42
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
43
|
+
fs.writeFileSync(file, content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function withTempRepo(run) {
|
|
47
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-release-preflight-'));
|
|
48
|
+
try {
|
|
49
|
+
return run(root);
|
|
50
|
+
} finally {
|
|
51
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The exact dependency closure release-preflight.mjs/benchmark-workflow.mjs
|
|
56
|
+
// need at runtime, copied verbatim (never reimplemented) under the same
|
|
57
|
+
// relative layout so change-firewall.cjs's own __dirname-based git-root
|
|
58
|
+
// resolution naturally targets this isolated checkout instead of the real one.
|
|
59
|
+
const RUNTIME_FILES = [
|
|
60
|
+
'src/claude/scripts/change-firewall.cjs',
|
|
61
|
+
'src/claude/scripts/spec-authoring-digest.cjs',
|
|
62
|
+
'src/claude/scripts/validate-spec-output.cjs',
|
|
63
|
+
'src/claude/scripts/workflow-policy.cjs',
|
|
64
|
+
'src/claude/scripts/spec-semantic-model.cjs',
|
|
65
|
+
'src/claude/scripts/spec-ground.cjs',
|
|
66
|
+
'scripts/release-preflight.mjs',
|
|
67
|
+
'scripts/benchmark-workflow.mjs',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function initIsolatedCheckout(root, { version = '0.0.1-fixture' } = {}) {
|
|
71
|
+
git(root, ['init', '-q']);
|
|
72
|
+
git(root, ['config', 'user.email', 'test@example.invalid']);
|
|
73
|
+
git(root, ['config', 'user.name', 'CafeKit Test']);
|
|
74
|
+
for (const relative of RUNTIME_FILES) {
|
|
75
|
+
const target = path.join(root, 'packages/spec', relative);
|
|
76
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
77
|
+
fs.copyFileSync(path.join(ROOT, relative), target);
|
|
78
|
+
}
|
|
79
|
+
writeJson(path.join(root, 'packages/spec/package.json'), { name: 'fixture-package', version });
|
|
80
|
+
fs.mkdirSync(path.join(root, 'packages/spec/benchmarks'), { recursive: true });
|
|
81
|
+
fs.mkdirSync(path.join(root, 'packages/spec/.cafekit-release'), { recursive: true });
|
|
82
|
+
fs.mkdirSync(path.join(root, 'packages/spec/bin/__tests__'), { recursive: true });
|
|
83
|
+
git(root, ['add', '-A']);
|
|
84
|
+
git(root, ['commit', '-qm', 'init']);
|
|
85
|
+
return git(root, ['rev-parse', 'HEAD']).trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runReleasePreflight(root) {
|
|
89
|
+
return spawnSync(process.execPath, [path.join(root, 'packages/spec/scripts/release-preflight.mjs')], {
|
|
90
|
+
cwd: root, encoding: 'utf8',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runFreezeCli(root, args = []) {
|
|
95
|
+
return spawnSync(process.execPath, [path.join(root, 'packages/spec/scripts/benchmark-workflow.mjs'), 'freeze', ...args], {
|
|
96
|
+
cwd: root, encoding: 'utf8',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Builds a durable Compact/Full authorized_evolution spec_ref fixture: a real
|
|
101
|
+
// requirements.md/design.md pair computeSemanticDigest21 accepts with zero
|
|
102
|
+
// errors, authoring both validated with a fresh C16 receipt, and a seed
|
|
103
|
+
// semantic_review_history PASS entry matching the real digest.
|
|
104
|
+
function buildEvolutionSpecFixture(root, name) {
|
|
105
|
+
const specDir = path.join(root, 'specs', name);
|
|
106
|
+
const requirementsPath = path.join(specDir, 'requirements.md');
|
|
107
|
+
const designPath = path.join(specDir, 'design.md');
|
|
108
|
+
writeFile(
|
|
109
|
+
requirementsPath,
|
|
110
|
+
'# Requirements\n\n### Requirement 1: One\n\n'
|
|
111
|
+
+ '- **R1.1** When input arrives, the service shall return one deterministic result.\n'
|
|
112
|
+
+ '- **R1.2** When the result is produced, the caller shall be able to verify status value 1.\n',
|
|
113
|
+
);
|
|
114
|
+
writeFile(
|
|
115
|
+
designPath,
|
|
116
|
+
'# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n'
|
|
117
|
+
+ '## Typed Anchors\n\n| ID | Type | Target | Role | Access | Action |\n|---|---|---|---|---|---|\n'
|
|
118
|
+
+ '| A-D-01 | file | `src/one.js` | runtime entrypoint | read | read |\n'
|
|
119
|
+
+ '| A-D-02 | command | `node --test test/one.test.js` | proof command | read | read |\n\n'
|
|
120
|
+
+ '## Decisions and Invariants\n\n### D1 — Stable result\n\nReturn status value 1 and reject invalid input.\n\n'
|
|
121
|
+
+ '### I1 — Invalid-state invariant\n\nInvalid input never reports the success status.\n\n'
|
|
122
|
+
+ '### C1 — Result contract\n\nThe result contains one numeric status.\n\n'
|
|
123
|
+
+ '## Verification Definitions\n\n'
|
|
124
|
+
+ '- **V1**: Subject criteria R1.1; Subject owner A-D-01; Proof criteria R1.2; Proof owner A-D-02; '
|
|
125
|
+
+ 'Evidence anchor A-D-02; Decision refs D1, I1, C1; Method command `node --test test/one.test.js`; '
|
|
126
|
+
+ 'Expected exit code 0 and status value 1; Negative/failure invalid input returns a deterministic error; '
|
|
127
|
+
+ 'Reachability/grounding entrypoint `src/one.js` via A-D-01, A-D-02.\n',
|
|
128
|
+
);
|
|
129
|
+
const policy = POLICY.workflowPolicySnapshot({ riskSignals: {}, planningDepth: 'Compact' });
|
|
130
|
+
const baseSpec = {
|
|
131
|
+
schema_version: '2.1',
|
|
132
|
+
feature_name: name,
|
|
133
|
+
created_at: '2026-08-16T00:00:00+07:00',
|
|
134
|
+
updated_at: '2026-08-16T00:00:00+07:00',
|
|
135
|
+
language: 'en',
|
|
136
|
+
status: 'in_progress',
|
|
137
|
+
current_phase: 'design',
|
|
138
|
+
scope_lock: { source: 'fixture', in_scope: [], out_of_scope: [], expansion_policy: 'requires-user-approval' },
|
|
139
|
+
coordination: { boundaries: [] },
|
|
140
|
+
authoring: { requirements: 'validated', design: 'validated', research: 'absent', tasks: 'absent' },
|
|
141
|
+
validation: { status: 'not-run' },
|
|
142
|
+
ready_for_implementation: false,
|
|
143
|
+
workflow_policy: policy,
|
|
144
|
+
};
|
|
145
|
+
const digestResult = VALIDATOR.computeSemanticDigest21(specDir, baseSpec);
|
|
146
|
+
assert.deepEqual(digestResult.errors, [], `fixture spec must compute cleanly: ${digestResult.errors.join('; ')}`);
|
|
147
|
+
const digest = digestResult.digest;
|
|
148
|
+
const receiptEntry = (bytesDigest) => ({ digest: bytesDigest, validated_at: '2026-08-16T00:00:01+07:00' });
|
|
149
|
+
const reviewReceiptDigest = cf.sha256Tag(`${name}-independent-pass-1`);
|
|
150
|
+
const fullSpec = {
|
|
151
|
+
...baseSpec,
|
|
152
|
+
validation: {
|
|
153
|
+
status: 'not-run',
|
|
154
|
+
authoring_validation: {
|
|
155
|
+
schema_version: '1',
|
|
156
|
+
requirements: receiptEntry(DIGEST.digestFileBytes(requirementsPath)),
|
|
157
|
+
design: receiptEntry(DIGEST.digestFileBytes(designPath)),
|
|
158
|
+
research: receiptEntry(DIGEST.RESEARCH_ABSENT_DIGEST),
|
|
159
|
+
tasks: receiptEntry(DIGEST.TASKS_ABSENT_DIGEST),
|
|
160
|
+
},
|
|
161
|
+
semantic_review_history: {
|
|
162
|
+
lineage_id: cf.sha256Tag(`${name}-lineage`),
|
|
163
|
+
entries: [{ sequence: 0, review_epoch: 0, verdict: 'PASS', semantic_digest: digest, review_receipt_digest: reviewReceiptDigest }],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
writeJson(path.join(specDir, 'spec.json'), fullSpec);
|
|
168
|
+
return { specRef: path.posix.join('specs', name), digest, reviewReceiptDigest };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Legitimate C10/C14 bootstrap + one authorized_evolution release cycle, built
|
|
172
|
+
// entirely through the production change-firewall.cjs API (bound to this
|
|
173
|
+
// isolated root) -- reaches a real `released` generation 1 authority with no
|
|
174
|
+
// hand-crafted digests. Returns the bound instance and key commits/digests.
|
|
175
|
+
function publishOneReleasedGeneration(root) {
|
|
176
|
+
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
|
|
177
|
+
schema_version: '1', verdict: 'PASS', blocking_count: 0, reviewed_criteria: ['R1.1'],
|
|
178
|
+
semantic_digest: cf.sha256Tag('bridge'), bootWindowBaseCommit: git(root, ['rev-parse', 'HEAD']).trim(),
|
|
179
|
+
written_at: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
const baseCommit = git(root, ['rev-parse', 'HEAD']).trim();
|
|
182
|
+
const reviewReceiptDigest = cf.sha256Tag('seed-review-receipt');
|
|
183
|
+
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), {
|
|
184
|
+
validation: {
|
|
185
|
+
semantic_review_history: {
|
|
186
|
+
entries: [{ sequence: 0, review_epoch: 0, verdict: 'PASS', review_receipt_digest: reviewReceiptDigest, semantic_digest: cf.sha256Tag('feature-digest') }],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
writeFile(path.join(root, 'packages/spec/src/claude/scripts/boot-window-change.cjs'), 'module.exports = {};\n');
|
|
191
|
+
git(root, ['add', '-A']);
|
|
192
|
+
git(root, ['commit', '-qm', 'boot window change']);
|
|
193
|
+
const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
|
|
194
|
+
|
|
195
|
+
const inst = cf.createChangeFirewall({ root });
|
|
196
|
+
inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
|
|
197
|
+
|
|
198
|
+
const changedPaths = ['packages/spec/src/claude/scripts/boot-window-change.cjs'];
|
|
199
|
+
const evo = buildEvolutionSpecFixture(root, 'cycle1-spec');
|
|
200
|
+
inst.createFreezeManifest({
|
|
201
|
+
change_intent: 'authorized_evolution', proposal_id: 'p1', spec_ref: evo.specRef,
|
|
202
|
+
spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
|
|
203
|
+
planned_write_set: changedPaths, rollback_condition: 'revert the commit',
|
|
204
|
+
negative_control_family: ['a write outside planned_write_set is rejected'],
|
|
205
|
+
});
|
|
206
|
+
const freeze = inst.verifyFreezeManifest();
|
|
207
|
+
return { inst, headCommit, changedPaths, freeze };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// release-preflight always requires a freeze (R1.4/R1.6)
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
test('release-preflight: exits 2 when no freeze exists at all (fresh boot-window checkout)', () => {
|
|
215
|
+
withTempRepo((root) => {
|
|
216
|
+
initIsolatedCheckout(root);
|
|
217
|
+
const result = runReleasePreflight(root);
|
|
218
|
+
assert.equal(result.status, 2);
|
|
219
|
+
assert.match(result.stderr, /release preflight failed/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// boot-window refusal: no_protected_change is only ever valid once released
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
test('release-preflight: refuses a no_protected_change freeze while authority is still in bootstrap state', () => {
|
|
228
|
+
withTempRepo((root) => {
|
|
229
|
+
const commit = initIsolatedCheckout(root);
|
|
230
|
+
// Baseline equal to current HEAD (empty derived diff) -- this exact shape
|
|
231
|
+
// can never be produced by the real createFreezeManifest during bootstrap
|
|
232
|
+
// (it refuses empty paths outright), so it is constructed directly here
|
|
233
|
+
// to prove the *reader* side (verifyFreezeManifest, via release-preflight)
|
|
234
|
+
// independently refuses it too, never trusting a stale/impossible freeze.
|
|
235
|
+
writeJson(path.join(root, 'packages/spec/benchmarks/release-baseline.json'), {
|
|
236
|
+
state: 'bootstrap', generation: 0, baselineCommit: commit,
|
|
237
|
+
baselinePackageVersion: '0.0.1-fixture', bootstrapReviewDigest: cf.sha256Tag('seed'),
|
|
238
|
+
previousBaselineDigest: null,
|
|
239
|
+
});
|
|
240
|
+
const inst = cf.createChangeFirewall({ root });
|
|
241
|
+
const authority = inst.loadBaselineAuthority();
|
|
242
|
+
const treeDigest = inst.computeProtectedTreeDigest([]);
|
|
243
|
+
const candidateFields = {
|
|
244
|
+
baselineAuthorityDigest: authority.digest, baselineGeneration: 0, baselineCommit: commit,
|
|
245
|
+
failureLedgerDigest: cf.NO_FAILURES_ATTESTATION_DIGEST, failureLedgerGeneration: null,
|
|
246
|
+
remediatedFailureIds: [], changedPaths: [], treeDigest,
|
|
247
|
+
proposalDigest: cf.NO_CHANGE_ATTESTATION_DIGEST, packageVersion: '0.0.1-fixture',
|
|
248
|
+
};
|
|
249
|
+
writeJson(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'), {
|
|
250
|
+
...candidateFields, changeKind: 'no_protected_change', candidateDigest: cf.computeCandidateDigest(candidateFields),
|
|
251
|
+
});
|
|
252
|
+
const result = runReleasePreflight(root);
|
|
253
|
+
assert.equal(result.status, 2);
|
|
254
|
+
assert.match(result.stderr, /release preflight failed/);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// released + no_protected_change succeeds (the normal clean-pack path)
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
test('release-preflight: released state with a clean no_protected_change freeze succeeds (exit 0)', () => {
|
|
263
|
+
withTempRepo((root) => {
|
|
264
|
+
initIsolatedCheckout(root);
|
|
265
|
+
const { inst } = publishOneReleasedGeneration(root);
|
|
266
|
+
const freezeBytes = fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'));
|
|
267
|
+
fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz'), 'fake-bytes');
|
|
268
|
+
const freezeJson = JSON.parse(freezeBytes.toString('utf8'));
|
|
269
|
+
const artifactDigest = cf.sha256Tag(fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz')));
|
|
270
|
+
const authorityBefore = inst.loadBaselineAuthority();
|
|
271
|
+
const receiptObj = {
|
|
272
|
+
schema_version: '1', receipt_id: 'r1', status: 'published',
|
|
273
|
+
baselineAuthorityDigest: authorityBefore.digest, baselineGeneration: authorityBefore.record.generation,
|
|
274
|
+
freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freezeJson.candidateDigest,
|
|
275
|
+
treeDigest: freezeJson.treeDigest, packageVersion: freezeJson.packageVersion,
|
|
276
|
+
releaseCommit: git(root, ['rev-parse', 'HEAD']).trim(),
|
|
277
|
+
artifactPath: 'packages/spec/.cafekit-release/artifact.tgz', artifactDigest, resolvedFailureIds: [],
|
|
278
|
+
};
|
|
279
|
+
inst.advanceBaseline(Buffer.from(`${JSON.stringify(receiptObj, null, 2)}\n`));
|
|
280
|
+
// released state, nothing further changed -> clean no_protected_change freeze.
|
|
281
|
+
inst.createFreezeManifest(null);
|
|
282
|
+
|
|
283
|
+
const result = runReleasePreflight(root);
|
|
284
|
+
assert.equal(result.status, 0, `stderr: ${result.stderr}`);
|
|
285
|
+
const parsed = JSON.parse(result.stdout);
|
|
286
|
+
assert.equal(parsed.status, 'preflight_ok');
|
|
287
|
+
assert.equal(parsed.changeKind, 'no_protected_change');
|
|
288
|
+
assert.equal(parsed.packageVersion, '0.0.1-fixture');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// R1.3 (design.md Errors and Recovery: "Caller base override | exit 2 | no
|
|
293
|
+
// accept"): a CLI/environment attempt to override the baseline this tool
|
|
294
|
+
// loads must be *rejected* with exit 2 -- not silently ignored -- and must
|
|
295
|
+
// never mutate the authority, receipt, or freeze files on its way to that
|
|
296
|
+
// rejection. release-preflight.mjs itself has no legitimate CLI argument at
|
|
297
|
+
// all (any argv is rejected before any filesystem access); the environment
|
|
298
|
+
// deny-boundary lives in change-firewall.cjs's shared assertBaselineUsable
|
|
299
|
+
// entry point (reached via verifyFreezeManifest) so it applies uniformly to
|
|
300
|
+
// every caller, not just this one CLI.
|
|
301
|
+
function setUpPublishedFixture(root) {
|
|
302
|
+
const { inst } = publishOneReleasedGeneration(root);
|
|
303
|
+
const freezeBytes = fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'));
|
|
304
|
+
fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz'), 'fake-bytes');
|
|
305
|
+
const freezeJson = JSON.parse(freezeBytes.toString('utf8'));
|
|
306
|
+
const artifactDigest = cf.sha256Tag(fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz')));
|
|
307
|
+
const authorityBefore = inst.loadBaselineAuthority();
|
|
308
|
+
const receiptObj = {
|
|
309
|
+
schema_version: '1', receipt_id: 'r1', status: 'published',
|
|
310
|
+
baselineAuthorityDigest: authorityBefore.digest, baselineGeneration: authorityBefore.record.generation,
|
|
311
|
+
freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freezeJson.candidateDigest,
|
|
312
|
+
treeDigest: freezeJson.treeDigest, packageVersion: freezeJson.packageVersion,
|
|
313
|
+
releaseCommit: git(root, ['rev-parse', 'HEAD']).trim(),
|
|
314
|
+
artifactPath: 'packages/spec/.cafekit-release/artifact.tgz', artifactDigest, resolvedFailureIds: [],
|
|
315
|
+
};
|
|
316
|
+
inst.advanceBaseline(Buffer.from(`${JSON.stringify(receiptObj, null, 2)}\n`));
|
|
317
|
+
inst.createFreezeManifest(null);
|
|
318
|
+
return { inst };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function snapshotPublishedFixtureBytes(root) {
|
|
322
|
+
return {
|
|
323
|
+
authority: fs.readFileSync(path.join(root, 'packages/spec/benchmarks/release-baseline.json')),
|
|
324
|
+
receipt: fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/release-receipt.json')),
|
|
325
|
+
freeze: fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json')),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
test('R1.3: release-preflight.mjs rejects any extra CLI argument with exit 2 and mutates nothing', () => {
|
|
330
|
+
withTempRepo((root) => {
|
|
331
|
+
initIsolatedCheckout(root);
|
|
332
|
+
setUpPublishedFixture(root);
|
|
333
|
+
const before = snapshotPublishedFixtureBytes(root);
|
|
334
|
+
|
|
335
|
+
const result = spawnSync(process.execPath, [
|
|
336
|
+
path.join(root, 'packages/spec/scripts/release-preflight.mjs'),
|
|
337
|
+
'--baseline-commit', '0'.repeat(40),
|
|
338
|
+
], { cwd: root, encoding: 'utf8' });
|
|
339
|
+
|
|
340
|
+
assert.equal(result.status, 2);
|
|
341
|
+
assert.match(result.stderr, /release preflight failed/);
|
|
342
|
+
assert.match(result.stderr, /accepts no CLI arguments/);
|
|
343
|
+
assert.deepEqual(snapshotPublishedFixtureBytes(root), before, 'a rejected CLI-argument attempt must mutate nothing');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('R1.3: presence of any of the six reserved CAFEKIT_ baseline-override env keys is rejected with exit 2 and mutates nothing', () => {
|
|
348
|
+
withTempRepo((root) => {
|
|
349
|
+
initIsolatedCheckout(root);
|
|
350
|
+
setUpPublishedFixture(root);
|
|
351
|
+
const before = snapshotPublishedFixtureBytes(root);
|
|
352
|
+
|
|
353
|
+
// The exact closed six-key reserved set (change-firewall.cjs R1.3) -- no
|
|
354
|
+
// other env name is ever a channel. Presence alone (any value) rejects.
|
|
355
|
+
for (const name of [
|
|
356
|
+
'CAFEKIT_BASE_COMMIT', 'CAFEKIT_BASELINE_COMMIT',
|
|
357
|
+
'CAFEKIT_BASE_REF', 'CAFEKIT_BASELINE_REF',
|
|
358
|
+
'CAFEKIT_BASE_PATH', 'CAFEKIT_BASELINE_PATH',
|
|
359
|
+
]) {
|
|
360
|
+
const result = spawnSync(process.execPath, [path.join(root, 'packages/spec/scripts/release-preflight.mjs')], {
|
|
361
|
+
cwd: root,
|
|
362
|
+
encoding: 'utf8',
|
|
363
|
+
// Test-created env only -- never the real process environment.
|
|
364
|
+
env: { PATH: process.env.PATH, [name]: '0'.repeat(40) },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
assert.equal(result.status, 2, `${name} must reject with exit 2`);
|
|
368
|
+
assert.match(result.stderr, /release preflight failed/);
|
|
369
|
+
assert.match(result.stderr, /reserved baseline override channel/);
|
|
370
|
+
assert.match(result.stderr, new RegExp(name));
|
|
371
|
+
assert.deepEqual(snapshotPublishedFixtureBytes(root), before, `${name} rejection must mutate nothing`);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('R1.3 control: no other environment variable name, however base/baseline-shaped or ambient, ever triggers the base-override deny-boundary', () => {
|
|
377
|
+
withTempRepo((root) => {
|
|
378
|
+
initIsolatedCheckout(root);
|
|
379
|
+
setUpPublishedFixture(root);
|
|
380
|
+
|
|
381
|
+
const result = spawnSync(process.execPath, [path.join(root, 'packages/spec/scripts/release-preflight.mjs')], {
|
|
382
|
+
cwd: root,
|
|
383
|
+
encoding: 'utf8',
|
|
384
|
+
// DATABASE_URL: token DATABASE, not BASE -- must never false-positive.
|
|
385
|
+
// RELEASE_NOTES_PATH: has a RELEASE-family token but no BASE/BASELINE
|
|
386
|
+
// token at all -- must never false-positive either. RELEASE_BASE_URL
|
|
387
|
+
// and AUTHORITY_BASE_URL end in BASE_URL, not BASE/BASELINE followed by
|
|
388
|
+
// COMMIT/REF/PATH. GITHUB_BASE_REF/GITHUB_BASE_SHA are ambient vars
|
|
389
|
+
// GitHub Actions sets on every pull_request run -- GITHUB is a
|
|
390
|
+
// third-party namespace this tool never owns. OVERRIDE_BASE_COMMIT,
|
|
391
|
+
// FORCE_BASELINE_REF, and BASECOMMIT are all base/baseline-shaped names
|
|
392
|
+
// that are NOT in the closed six-key reserved set -- an env key this
|
|
393
|
+
// runtime never reads cannot influence authority, so none of these
|
|
394
|
+
// may ever be treated as a channel this boundary owns (see
|
|
395
|
+
// change-firewall.cjs R1.3).
|
|
396
|
+
env: {
|
|
397
|
+
PATH: process.env.PATH,
|
|
398
|
+
DATABASE_URL: 'postgres://example',
|
|
399
|
+
RELEASE_NOTES_PATH: '/tmp/notes.md',
|
|
400
|
+
RELEASE_BASE_URL: 'https://example.invalid/release',
|
|
401
|
+
AUTHORITY_BASE_URL: 'https://example.invalid/authority',
|
|
402
|
+
GITHUB_BASE_REF: 'main',
|
|
403
|
+
GITHUB_BASE_SHA: '0'.repeat(40),
|
|
404
|
+
OVERRIDE_BASE_COMMIT: '0'.repeat(40),
|
|
405
|
+
FORCE_BASELINE_REF: 'main',
|
|
406
|
+
BASECOMMIT: '0'.repeat(40),
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
assert.equal(result.status, 0, `stderr: ${result.stderr}`);
|
|
411
|
+
const parsed = JSON.parse(result.stdout);
|
|
412
|
+
assert.equal(parsed.status, 'preflight_ok');
|
|
413
|
+
assert.equal(parsed.changeKind, 'no_protected_change');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// stale freeze after a baseline advance, and after a ledger event (R1.4/R1.10)
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
test('release-preflight: exits 2 for a freeze left stale by a baseline advance since it was created', () => {
|
|
422
|
+
withTempRepo((root) => {
|
|
423
|
+
initIsolatedCheckout(root);
|
|
424
|
+
const { inst } = publishOneReleasedGeneration(root);
|
|
425
|
+
const staleFreezeBytes = fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'));
|
|
426
|
+
const staleFreezeJson = JSON.parse(staleFreezeBytes.toString('utf8'));
|
|
427
|
+
fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz'), 'fake-bytes');
|
|
428
|
+
const artifactDigest = cf.sha256Tag(fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz')));
|
|
429
|
+
const authorityBefore = inst.loadBaselineAuthority();
|
|
430
|
+
const receiptObj = {
|
|
431
|
+
schema_version: '1', receipt_id: 'r1', status: 'published',
|
|
432
|
+
baselineAuthorityDigest: authorityBefore.digest, baselineGeneration: authorityBefore.record.generation,
|
|
433
|
+
freezeDigest: cf.sha256Tag(staleFreezeBytes), candidateDigest: staleFreezeJson.candidateDigest,
|
|
434
|
+
treeDigest: staleFreezeJson.treeDigest, packageVersion: staleFreezeJson.packageVersion,
|
|
435
|
+
releaseCommit: git(root, ['rev-parse', 'HEAD']).trim(),
|
|
436
|
+
artifactPath: 'packages/spec/.cafekit-release/artifact.tgz', artifactDigest, resolvedFailureIds: [],
|
|
437
|
+
};
|
|
438
|
+
// advanceBaseline rotates authority to generation 1 and invalidates the freeze;
|
|
439
|
+
// restore the pre-advance (now stale) freeze bytes to simulate one left on disk.
|
|
440
|
+
inst.advanceBaseline(Buffer.from(`${JSON.stringify(receiptObj, null, 2)}\n`));
|
|
441
|
+
fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'), staleFreezeBytes);
|
|
442
|
+
|
|
443
|
+
const result = runReleasePreflight(root);
|
|
444
|
+
assert.equal(result.status, 2);
|
|
445
|
+
assert.match(result.stderr, /release preflight failed/);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('release-preflight: exits 2 for a freeze left stale by a C15 ledger event since it was created', () => {
|
|
450
|
+
withTempRepo((root) => {
|
|
451
|
+
initIsolatedCheckout(root);
|
|
452
|
+
const { inst } = publishOneReleasedGeneration(root);
|
|
453
|
+
fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz'), 'fake-bytes');
|
|
454
|
+
const artifactDigest = cf.sha256Tag(fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz')));
|
|
455
|
+
const freezeBytes = fs.readFileSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'));
|
|
456
|
+
const freezeJson = JSON.parse(freezeBytes.toString('utf8'));
|
|
457
|
+
const authorityBefore = inst.loadBaselineAuthority();
|
|
458
|
+
inst.advanceBaseline(Buffer.from(`${JSON.stringify({
|
|
459
|
+
schema_version: '1', receipt_id: 'r1', status: 'published',
|
|
460
|
+
baselineAuthorityDigest: authorityBefore.digest, baselineGeneration: authorityBefore.record.generation,
|
|
461
|
+
freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freezeJson.candidateDigest,
|
|
462
|
+
treeDigest: freezeJson.treeDigest, packageVersion: freezeJson.packageVersion,
|
|
463
|
+
releaseCommit: git(root, ['rev-parse', 'HEAD']).trim(),
|
|
464
|
+
artifactPath: 'packages/spec/.cafekit-release/artifact.tgz', artifactDigest, resolvedFailureIds: [],
|
|
465
|
+
}, null, 2)}\n`));
|
|
466
|
+
// fresh clean freeze against the now-released authority
|
|
467
|
+
inst.createFreezeManifest(null);
|
|
468
|
+
// a ledger event lands after the freeze was created -> stales it (R1.10)
|
|
469
|
+
inst.openFailure({
|
|
470
|
+
failure_id: 'F-stale', paths: ['packages/spec/scripts/unrelated.mjs'], candidate_digest: cf.sha256Tag('c'),
|
|
471
|
+
evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const result = runReleasePreflight(root);
|
|
475
|
+
assert.equal(result.status, 2);
|
|
476
|
+
assert.match(result.stderr, /release preflight failed/);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// benchmark-workflow.mjs freeze CLI: proposal variant failures
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
test('benchmark-workflow freeze CLI: refuses a protected change with no --proposal supplied at all', () => {
|
|
485
|
+
withTempRepo((root) => {
|
|
486
|
+
initIsolatedCheckout(root);
|
|
487
|
+
publishOneReleasedGeneration(root);
|
|
488
|
+
writeFile(path.join(root, 'packages/spec/scripts/needs-proposal.mjs'), 'export const x = 1;\n');
|
|
489
|
+
git(root, ['add', '-A']);
|
|
490
|
+
git(root, ['commit', '-qm', 'unauthorized-looking change']);
|
|
491
|
+
|
|
492
|
+
const result = runFreezeCli(root, []);
|
|
493
|
+
assert.equal(result.status, 2);
|
|
494
|
+
assert.match(result.stderr, /benchmark validation failed/);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('benchmark-workflow freeze CLI: refuses a benchmark_remediation proposal for a non-framework_regression class', () => {
|
|
499
|
+
withTempRepo((root) => {
|
|
500
|
+
initIsolatedCheckout(root);
|
|
501
|
+
publishOneReleasedGeneration(root);
|
|
502
|
+
writeFile(path.join(root, 'packages/spec/scripts/regressed.mjs'), 'export const x = 1;\n');
|
|
503
|
+
git(root, ['add', '-A']);
|
|
504
|
+
git(root, ['commit', '-qm', 'regressed change']);
|
|
505
|
+
const proposalFile = path.join(root, 'proposal.json');
|
|
506
|
+
writeJson(proposalFile, {
|
|
507
|
+
change_intent: 'benchmark_remediation', proposal_id: 'p', invariant_id: 'inv', hypothesis: 'h',
|
|
508
|
+
owner_layer: 'benchmark_controller', primary_failure_class: 'oracle_defect',
|
|
509
|
+
paths: ['packages/spec/scripts/regressed.mjs'], failure_ids: ['F1'], evidence: ['e'],
|
|
510
|
+
true_positive_family: ['tp'], negative_control_family: ['nc'], rollback_condition: 'revert',
|
|
511
|
+
});
|
|
512
|
+
const result = runFreezeCli(root, ['--proposal', proposalFile]);
|
|
513
|
+
assert.equal(result.status, 2);
|
|
514
|
+
assert.match(result.stderr, /non_framework_regression|benchmark validation failed/);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('benchmark-workflow freeze CLI: refuses an authorized_evolution proposal missing required fields', () => {
|
|
519
|
+
withTempRepo((root) => {
|
|
520
|
+
initIsolatedCheckout(root);
|
|
521
|
+
publishOneReleasedGeneration(root);
|
|
522
|
+
writeFile(path.join(root, 'packages/spec/scripts/evolved.mjs'), 'export const x = 1;\n');
|
|
523
|
+
git(root, ['add', '-A']);
|
|
524
|
+
git(root, ['commit', '-qm', 'evolved change']);
|
|
525
|
+
const proposalFile = path.join(root, 'proposal.json');
|
|
526
|
+
writeJson(proposalFile, {
|
|
527
|
+
// missing spec_semantic_digest / independent_pass_receipt_digest / negative_control_family
|
|
528
|
+
change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: 'specs/does-not-exist',
|
|
529
|
+
planned_write_set: ['packages/spec/scripts/evolved.mjs'], rollback_condition: 'r',
|
|
530
|
+
});
|
|
531
|
+
const result = runFreezeCli(root, ['--proposal', proposalFile]);
|
|
532
|
+
assert.equal(result.status, 2);
|
|
533
|
+
assert.match(result.stderr, /malformed_proposal|benchmark validation failed/);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// sentinel: any injected defect surfaces as a nonzero exit, never a false success
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
test('sentinel: a corrupted freeze file surfaces as release-preflight exit 2, never a false success', () => {
|
|
542
|
+
withTempRepo((root) => {
|
|
543
|
+
initIsolatedCheckout(root);
|
|
544
|
+
publishOneReleasedGeneration(root);
|
|
545
|
+
fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'), '{ not valid json');
|
|
546
|
+
const result = runReleasePreflight(root);
|
|
547
|
+
assert.equal(result.status, 2);
|
|
548
|
+
assert.match(result.stderr, /release preflight failed/);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// package.json wiring: version preserved, reserved paths excluded (real checkout, read-only)
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
test('package.json: version stays valid SemVer and the npm lifecycle stays unwired while the boot-window is open', () => {
|
|
557
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
558
|
+
// R1.6 requires the version field preserved, not pinned to one release literal
|
|
559
|
+
// this task happened to land against -- a hard-coded release string would
|
|
560
|
+
// need re-patching on every version bump without proving anything R1.6 asks
|
|
561
|
+
// for. The durable, release-agnostic invariant is: the field is present and
|
|
562
|
+
// is a valid SemVer string (optional prerelease/build metadata included).
|
|
563
|
+
assert.equal(typeof pkg.version, 'string');
|
|
564
|
+
assert.match(pkg.version, /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
565
|
+
// R0-01 wired prepack/prepublishOnly to release-preflight.mjs while stating
|
|
566
|
+
// it "does not invoke bootstrapBaseline, create a real freeze, or publish
|
|
567
|
+
// (D11/I9 boot-window)". R1-03 bootstrap-activation, which closes that
|
|
568
|
+
// window, never ran and this feature is paused -- so the wiring made every
|
|
569
|
+
// publish exit 2 on a freeze that nothing was allowed to create. The gate
|
|
570
|
+
// stays runnable on its own (`release:freeze`, and the script invoked
|
|
571
|
+
// directly by the tests above); it just no longer sits on npm's lifecycle.
|
|
572
|
+
// Rewire both when R1-03 lands.
|
|
573
|
+
assert.equal(pkg.scripts.prepack, undefined);
|
|
574
|
+
assert.equal(pkg.scripts.prepublishOnly, undefined);
|
|
575
|
+
assert.equal(pkg.scripts['release:freeze'], 'node scripts/benchmark-workflow.mjs freeze');
|
|
576
|
+
// pre-existing scripts untouched
|
|
577
|
+
assert.equal(pkg.scripts.test, 'node scripts/run-skill-self-tests.mjs');
|
|
578
|
+
assert.equal(pkg.scripts['test:package'], 'node --test bin/__tests__/package-inventory.test.js');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('D1: reserved control-plane paths never appear inside derived protected changes', () => {
|
|
582
|
+
assert.equal(cf.isProtectedPath('packages/spec/benchmarks/release-baseline.json'), false);
|
|
583
|
+
assert.equal(cf.isProtectedPath('packages/spec/benchmarks/benchmark-failure-ledger.json'), false);
|
|
584
|
+
assert.equal(cf.isProtectedPath('packages/spec/.cafekit-release/change-firewall-freeze.json'), false);
|
|
585
|
+
assert.equal(cf.isProtectedPath('packages/spec/.cafekit-release/release-receipt.json'), false);
|
|
586
|
+
});
|