@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
  2. package/bin/__tests__/codex-hooks.test.js +531 -29
  3. package/bin/__tests__/codex-native.test.js +228 -7
  4. package/bin/__tests__/develop-contract.test.js +653 -185
  5. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
  6. package/bin/__tests__/installer-safety.test.js +39 -0
  7. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  8. package/bin/__tests__/package-inventory.test.js +705 -13
  9. package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
  10. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
  11. package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
  12. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
  13. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
  14. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  15. package/bin/__tests__/validator-grounding.test.js +2013 -221
  16. package/bin/lib/managed-writer.js +12 -3
  17. package/bin/phases/codex-runtime.js +17 -1
  18. package/package.json +5 -2
  19. package/src/claude/CLAUDE.md +6 -0
  20. package/src/claude/agents/code-auditor.md +20 -0
  21. package/src/claude/agents/spec-maker.md +143 -99
  22. package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
  23. package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
  24. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  25. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  26. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  27. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  28. package/src/claude/hooks/completion-authority.cjs +13 -7
  29. package/src/claude/hooks/docs-sync.cjs +5 -2
  30. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  31. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  32. package/src/claude/hooks/privacy-block.cjs +99 -28
  33. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  34. package/src/claude/hooks/spec-gate.cjs +47 -117
  35. package/src/claude/hooks/spec-state.cjs +8 -2
  36. package/src/claude/migration-manifest.json +9 -1
  37. package/src/claude/rules/state-sync.md +52 -21
  38. package/src/claude/rules/workflow.md +17 -9
  39. package/src/claude/scripts/change-firewall.cjs +1748 -0
  40. package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
  41. package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
  42. package/src/claude/scripts/spec-final-state.cjs +290 -0
  43. package/src/claude/scripts/spec-ground.cjs +571 -173
  44. package/src/claude/scripts/spec-readiness.cjs +425 -0
  45. package/src/claude/scripts/spec-receipt.cjs +162 -0
  46. package/src/claude/scripts/spec-resolver.cjs +67 -16
  47. package/src/claude/scripts/spec-scaffold.cjs +1594 -139
  48. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  49. package/src/claude/scripts/validate-spec-output.cjs +3110 -218
  50. package/src/claude/scripts/workflow-policy.cjs +501 -183
  51. package/src/claude/settings/settings.json +4 -0
  52. package/src/claude/skills/code-review/SKILL.md +12 -3
  53. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  54. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  55. package/src/claude/skills/develop/SKILL.md +47 -60
  56. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  57. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  58. package/src/claude/skills/specs/SKILL.md +183 -177
  59. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  60. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  61. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  62. package/src/claude/skills/specs/references/review.md +214 -262
  63. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  64. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  65. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  66. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  67. package/src/claude/skills/specs/rules/design-review.md +42 -97
  68. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  69. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  70. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  71. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  72. package/src/claude/skills/specs/templates/design.md +49 -263
  73. package/src/claude/skills/specs/templates/requirements.md +26 -36
  74. package/src/claude/skills/specs/templates/research.md +9 -98
  75. package/src/claude/skills/specs/templates/spec-state.json +35 -85
  76. package/src/claude/skills/specs/templates/task.md +30 -102
  77. package/src/claude/skills/sync/SKILL.md +33 -10
  78. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  79. package/src/claude/skills/test/SKILL.md +17 -7
  80. package/src/codex/AGENTS.md +6 -0
  81. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  82. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  83. package/src/codex/hooks/completion-authority.cjs +13 -7
  84. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  85. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  86. package/src/codex/hooks/lib/privacy-state.cjs +61 -38
  87. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  88. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  89. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  90. package/src/codex/hooks/privacy-approval.cjs +6 -2
  91. package/src/codex/hooks/privacy-block.cjs +94 -15
  92. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  93. package/src/codex/hooks/spec-gate.cjs +49 -34
  94. package/src/codex/hooks/spec-state.cjs +10 -3
  95. package/src/codex/hooks/state.cjs +2 -0
  96. package/src/codex/hooks.json +6 -1
  97. package/src/codex/rules/state-sync.md +47 -27
  98. package/src/common/AGENTS.md +1 -1
@@ -0,0 +1,578 @@
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 (release-agnostic) and prepack/prepublishOnly wired to release-preflight.mjs', () => {
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
+ assert.equal(pkg.scripts.prepack, 'node scripts/release-preflight.mjs');
566
+ assert.equal(pkg.scripts.prepublishOnly, 'node scripts/release-preflight.mjs');
567
+ assert.equal(pkg.scripts['release:freeze'], 'node scripts/benchmark-workflow.mjs freeze');
568
+ // pre-existing scripts untouched
569
+ assert.equal(pkg.scripts.test, 'node scripts/run-skill-self-tests.mjs');
570
+ assert.equal(pkg.scripts['test:package'], 'node --test bin/__tests__/package-inventory.test.js');
571
+ });
572
+
573
+ test('D1: reserved control-plane paths never appear inside derived protected changes', () => {
574
+ assert.equal(cf.isProtectedPath('packages/spec/benchmarks/release-baseline.json'), false);
575
+ assert.equal(cf.isProtectedPath('packages/spec/benchmarks/benchmark-failure-ledger.json'), false);
576
+ assert.equal(cf.isProtectedPath('packages/spec/.cafekit-release/change-firewall-freeze.json'), false);
577
+ assert.equal(cf.isProtectedPath('packages/spec/.cafekit-release/release-receipt.json'), false);
578
+ });