@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,2134 @@
1
+ 'use strict';
2
+
3
+ // R0-01 owned semantic-firewall test for change-firewall.cjs (D1/C1/C9/C10/C14/C15/C16).
4
+ // Every mutation below runs against an isolated temp git repo constructed by
5
+ // this file; the real project checkout's own change-firewall authority is
6
+ // never touched, except the one, explicitly read-only precondition check
7
+ // below (D11/R1.14), which must run before any other assertion in this file.
8
+
9
+ const assert = require('node:assert/strict');
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+ const { spawnSync } = require('node:child_process');
14
+ const { test } = require('node:test');
15
+
16
+ const ROOT = path.resolve(__dirname, '../..');
17
+ const cf = require(path.join(ROOT, 'src/claude/scripts/change-firewall.cjs'));
18
+ const DIGEST = require(path.join(ROOT, 'src/claude/scripts/spec-authoring-digest.cjs'));
19
+ const VALIDATOR = require(path.join(ROOT, 'src/claude/scripts/validate-spec-output.cjs'));
20
+ const POLICY = require(path.join(ROOT, 'src/claude/scripts/workflow-policy.cjs'));
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // generic fixture helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function git(root, args) {
27
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
28
+ assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stdout}\n${result.stderr}`);
29
+ return result.stdout;
30
+ }
31
+
32
+ function writeJson(file, value) {
33
+ fs.mkdirSync(path.dirname(file), { recursive: true });
34
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
35
+ }
36
+
37
+ function writeFile(file, content) {
38
+ fs.mkdirSync(path.dirname(file), { recursive: true });
39
+ fs.writeFileSync(file, content);
40
+ }
41
+
42
+ function withTempRepo(run) {
43
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-change-firewall-'));
44
+ try {
45
+ return run(root);
46
+ } finally {
47
+ fs.rmSync(root, { recursive: true, force: true });
48
+ }
49
+ }
50
+
51
+ // Minimal repo skeleton: git init + the exact directory shape D1/C10/C15 read
52
+ // from, plus a committed packages/spec/package.json.
53
+ function initRepo(root, { version = '0.0.1-fixture' } = {}) {
54
+ git(root, ['init', '-q']);
55
+ git(root, ['config', 'user.email', 'test@example.invalid']);
56
+ git(root, ['config', 'user.name', 'CafeKit Test']);
57
+ writeJson(path.join(root, 'packages/spec/package.json'), { name: 'fixture-package', version });
58
+ fs.mkdirSync(path.join(root, 'packages/spec/benchmarks'), { recursive: true });
59
+ fs.mkdirSync(path.join(root, 'packages/spec/.cafekit-release'), { recursive: true });
60
+ fs.mkdirSync(path.join(root, 'packages/spec/src/claude/scripts'), { recursive: true });
61
+ fs.mkdirSync(path.join(root, 'packages/spec/scripts'), { recursive: true });
62
+ fs.mkdirSync(path.join(root, 'packages/spec/bin/__tests__'), { recursive: true });
63
+ git(root, ['add', '-A']);
64
+ git(root, ['commit', '-qm', 'init']);
65
+ return git(root, ['rev-parse', 'HEAD']).trim();
66
+ }
67
+
68
+ function writeLegacyBridge(root, { baseCommit, semanticDigest = cf.sha256Tag('bridge-digest') }) {
69
+ writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
70
+ schema_version: '1',
71
+ verdict: 'PASS',
72
+ blocking_count: 0,
73
+ reviewed_criteria: ['R1.1'],
74
+ semantic_digest: semanticDigest,
75
+ bootWindowBaseCommit: baseCommit,
76
+ written_at: new Date().toISOString(),
77
+ });
78
+ }
79
+
80
+ function writeFeatureSeedPass(root, { reviewReceiptDigest = cf.sha256Tag('seed-review-receipt') } = {}) {
81
+ writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), {
82
+ validation: {
83
+ semantic_review_history: {
84
+ entries: [{
85
+ sequence: 0, review_epoch: 0, verdict: 'PASS',
86
+ review_receipt_digest: reviewReceiptDigest, semantic_digest: cf.sha256Tag('feature-digest'),
87
+ }],
88
+ },
89
+ },
90
+ });
91
+ return reviewReceiptDigest;
92
+ }
93
+
94
+ // Builds a durable Compact/Full authorized_evolution spec_ref fixture: a real
95
+ // requirements.md/design.md pair that computeSemanticDigest21 accepts with
96
+ // zero errors, authoring both validated with a fresh C16 receipt, and an
97
+ // optional seed semantic_review_history PASS entry matching the real digest.
98
+ function buildEvolutionSpecFixture(root, {
99
+ name, planningDepth = 'Compact', includeSeedPass = true, freshReceipt = true,
100
+ } = {}) {
101
+ const specDir = path.join(root, 'specs', name);
102
+ const requirementsPath = path.join(specDir, 'requirements.md');
103
+ const designPath = path.join(specDir, 'design.md');
104
+ writeFile(
105
+ requirementsPath,
106
+ '# Requirements\n\n### Requirement 1: One\n\n'
107
+ + '- **R1.1** When input arrives, the service shall return one deterministic result.\n'
108
+ + '- **R1.2** When the result is produced, the caller shall be able to verify status value 1.\n',
109
+ );
110
+ writeFile(
111
+ designPath,
112
+ '# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n'
113
+ + '## Typed Anchors\n\n| ID | Type | Target | Role | Access | Action |\n|---|---|---|---|---|---|\n'
114
+ + '| A-D-01 | file | `src/one.js` | runtime entrypoint | read | read |\n'
115
+ + '| A-D-02 | command | `node --test test/one.test.js` | proof command | read | read |\n\n'
116
+ + '## Decisions and Invariants\n\n### D1 — Stable result\n\nReturn status value 1 and reject invalid input.\n\n'
117
+ + '### I1 — Invalid-state invariant\n\nInvalid input never reports the success status.\n\n'
118
+ + '### C1 — Result contract\n\nThe result contains one numeric status.\n\n'
119
+ + '## Verification Definitions\n\n'
120
+ + '- **V1**: Subject criteria R1.1; Subject owner A-D-01; Proof criteria R1.2; Proof owner A-D-02; '
121
+ + 'Evidence anchor A-D-02; Decision refs D1, I1, C1; Method command `node --test test/one.test.js`; '
122
+ + 'Expected exit code 0 and status value 1; Negative/failure invalid input returns a deterministic error; '
123
+ + 'Reachability/grounding entrypoint `src/one.js` via A-D-01, A-D-02.\n',
124
+ );
125
+ const policy = POLICY.workflowPolicySnapshot({ riskSignals: {}, planningDepth });
126
+ const baseSpec = {
127
+ schema_version: '2.1',
128
+ feature_name: name,
129
+ created_at: '2026-08-16T00:00:00+07:00',
130
+ updated_at: '2026-08-16T00:00:00+07:00',
131
+ language: 'en',
132
+ status: 'in_progress',
133
+ current_phase: 'design',
134
+ scope_lock: { source: 'fixture', in_scope: [], out_of_scope: [], expansion_policy: 'requires-user-approval' },
135
+ coordination: { boundaries: [] },
136
+ authoring: { requirements: 'validated', design: 'validated', research: 'absent', tasks: 'absent' },
137
+ validation: { status: 'not-run' },
138
+ ready_for_implementation: false,
139
+ workflow_policy: policy,
140
+ };
141
+ const digestResult = VALIDATOR.computeSemanticDigest21(specDir, baseSpec);
142
+ assert.deepEqual(digestResult.errors, [], `fixture spec must compute cleanly: ${digestResult.errors.join('; ')}`);
143
+ const digest = digestResult.digest;
144
+
145
+ const receiptEntry = (bytesDigest) => ({ digest: bytesDigest, validated_at: '2026-08-16T00:00:01+07:00' });
146
+ const authoringValidation = freshReceipt ? {
147
+ schema_version: '1',
148
+ requirements: receiptEntry(DIGEST.digestFileBytes(requirementsPath)),
149
+ design: receiptEntry(DIGEST.digestFileBytes(designPath)),
150
+ research: receiptEntry(DIGEST.RESEARCH_ABSENT_DIGEST),
151
+ tasks: receiptEntry(DIGEST.TASKS_ABSENT_DIGEST),
152
+ } : null;
153
+
154
+ const reviewReceiptDigest = cf.sha256Tag(`${name}-independent-pass-1`);
155
+ const entries = includeSeedPass ? [{
156
+ sequence: 0, review_epoch: 0, verdict: 'PASS',
157
+ semantic_digest: digest, review_receipt_digest: reviewReceiptDigest,
158
+ }] : [];
159
+
160
+ const fullSpec = {
161
+ ...baseSpec,
162
+ validation: {
163
+ status: 'not-run',
164
+ authoring_validation: authoringValidation,
165
+ semantic_review_history: { lineage_id: cf.sha256Tag(`${name}-lineage`), entries },
166
+ },
167
+ };
168
+ writeJson(path.join(specDir, 'spec.json'), fullSpec);
169
+ return {
170
+ specDir, specRef: path.posix.join('specs', name), digest, reviewReceiptDigest, requirementsPath, designPath,
171
+ };
172
+ }
173
+
174
+ function readFileBytes(...segments) {
175
+ return fs.readFileSync(path.join(...segments));
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // R1.14 / D11 precondition -- MUST run before any other change-firewall
180
+ // assertion in this file. Reads the real project checkout only; never writes it.
181
+ // ---------------------------------------------------------------------------
182
+
183
+ test('R1.14 precondition: the real legacy-bridge artifact exists and matches its exact schema before any other assertion', () => {
184
+ const parsed = cf.assertLegacyBridgeArtifact();
185
+ assert.equal(parsed.schema_version, '1');
186
+ assert.equal(parsed.verdict, 'PASS');
187
+ assert.equal(parsed.blocking_count, 0);
188
+ assert.ok(Array.isArray(parsed.reviewed_criteria) && parsed.reviewed_criteria.length > 0);
189
+ assert.match(parsed.semantic_digest, /^sha256:[0-9a-f]{64}$/);
190
+ assert.match(parsed.bootWindowBaseCommit, /^[0-9a-f]{40}$/);
191
+ assert.equal(typeof parsed.written_at, 'string');
192
+ assert.deepEqual(
193
+ Object.keys(parsed).sort(),
194
+ ['blocking_count', 'bootWindowBaseCommit', 'reviewed_criteria', 'schema_version', 'semantic_digest', 'verdict', 'written_at'],
195
+ );
196
+ });
197
+
198
+ test('R1.14 negative: an absent or malformed legacy-bridge artifact fails the precondition nonzero', () => {
199
+ withTempRepo((root) => {
200
+ initRepo(root);
201
+ const inst = cf.createChangeFirewall({ root });
202
+ assert.throws(() => inst.assertLegacyBridgeArtifact(), (error) => error.code === 'legacy_bridge_missing');
203
+
204
+ writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
205
+ schema_version: '1', verdict: 'FAIL', blocking_count: 0,
206
+ reviewed_criteria: ['R1.1'], semantic_digest: cf.sha256Tag('x'),
207
+ bootWindowBaseCommit: '0'.repeat(40), written_at: new Date().toISOString(),
208
+ });
209
+ assert.throws(() => inst.assertLegacyBridgeArtifact(), (error) => error.code === 'legacy_bridge_malformed');
210
+ });
211
+ });
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // D1 allowlist (R1.1 scaffolding)
215
+ // ---------------------------------------------------------------------------
216
+
217
+ test('D1: reserved control-plane paths are excluded from the protected allowlist before the prefix match', () => {
218
+ assert.equal(cf.isProtectedPath('packages/spec/benchmarks/release-baseline.json'), false);
219
+ assert.equal(cf.isProtectedPath('packages/spec/benchmarks/benchmark-failure-ledger.json'), false);
220
+ assert.equal(cf.isProtectedPath('packages/spec/benchmarks/other-fixture.json'), true);
221
+ assert.equal(cf.isProtectedPath('packages/spec/.cafekit-release/change-firewall-freeze.json'), false);
222
+ assert.equal(cf.isProtectedPath('packages/spec/src/claude/scripts/foo.cjs'), true);
223
+ assert.equal(cf.isProtectedPath('packages/spec/scripts/bar.mjs'), true);
224
+ assert.equal(cf.isProtectedPath('packages/spec/bin/__tests__/x.test.js'), true);
225
+ assert.equal(cf.isProtectedPath('packages/spec/bin/phases/copy-payload.js'), true);
226
+ assert.equal(cf.isProtectedPath('packages/spec/package.json'), true);
227
+ assert.equal(cf.isProtectedPath('packages/spec/.gitignore'), true);
228
+ assert.equal(cf.isProtectedPath('packages/spec/README.md'), false);
229
+ });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Boot-window absence (this task's authority decision): the real checkout's
233
+ // own release-baseline.json / benchmark-failure-ledger.json do not exist yet
234
+ // (R0-01 never invokes bootstrapBaseline/openFailure); loaders must read this
235
+ // as the exact, contract-legitimate "absent" state, not an error.
236
+ // ---------------------------------------------------------------------------
237
+
238
+ test('boot-window absence: loadBaselineAuthority/loadFailureLedger read the real checkout state as legitimate absent, not malformed', () => {
239
+ assert.deepEqual(cf.loadBaselineAuthority(), { state: 'absent' });
240
+ assert.deepEqual(cf.loadFailureLedger(), { state: 'absent' });
241
+ assert.equal(cf.computeFailureLedgerDigest(), cf.NO_FAILURES_ATTESTATION_DIGEST);
242
+ assert.throws(() => cf.assertBaselineUsable(), (error) => error.code === 'authority_absent');
243
+ // assertFailureLedgerUsable never throws on legitimate absence (C15: "zero-open-failures", not malformed).
244
+ assert.deepEqual(cf.assertFailureLedgerUsable(), { state: 'absent' });
245
+ });
246
+
247
+ test('R1.3: assertNoBaseOverrideEnv rejects only the exact six reserved CAFEKIT_ keys, by name only, never a name-shape guess', () => {
248
+ function withEnvVar(name, value, run) {
249
+ const had = Object.prototype.hasOwnProperty.call(process.env, name);
250
+ const previous = process.env[name];
251
+ process.env[name] = value;
252
+ try {
253
+ return run();
254
+ } finally {
255
+ if (had) process.env[name] = previous; else delete process.env[name];
256
+ }
257
+ }
258
+
259
+ // Positive: the exact closed six-key reserved set -- presence alone (any
260
+ // value) must reject exit 2 and must never mutate the real checkout's own
261
+ // (boot-window absent) authority state.
262
+ for (const name of [
263
+ 'CAFEKIT_BASE_COMMIT', 'CAFEKIT_BASELINE_COMMIT',
264
+ 'CAFEKIT_BASE_REF', 'CAFEKIT_BASELINE_REF',
265
+ 'CAFEKIT_BASE_PATH', 'CAFEKIT_BASELINE_PATH',
266
+ ]) {
267
+ withEnvVar(name, 'attempted-override', () => {
268
+ assert.throws(() => cf.assertBaselineUsable(), (error) => error.code === 'caller_base_override', `${name} must be rejected`);
269
+ assert.deepEqual(cf.loadBaselineAuthority(), { state: 'absent' }, `${name} rejection must not mutate authority state`);
270
+ });
271
+ }
272
+
273
+ // Negative controls: no other environment variable name is ever a channel,
274
+ // however base/baseline-shaped or ambient -- presence must never influence
275
+ // behavior, so the real absent-authority checkout state must still surface,
276
+ // not a false reject. GITHUB_BASE_REF/GITHUB_BASE_SHA are ambient vars
277
+ // GitHub Actions sets on every pull_request run; OVERRIDE_BASE_COMMIT,
278
+ // FORCE_BASELINE_REF, and BASECOMMIT are base/baseline-shaped names that
279
+ // are NOT in the closed six-key reserved set; RELEASE_BASE_URL,
280
+ // AUTHORITY_BASE_URL, DATABASE_URL, and RELEASE_NOTES_PATH share no exact
281
+ // reserved name either -- none of these are ever read for authority.
282
+ for (const name of [
283
+ 'RELEASE_BASE_URL', 'AUTHORITY_BASE_URL', 'DATABASE_URL', 'RELEASE_NOTES_PATH',
284
+ 'GITHUB_BASE_REF', 'GITHUB_BASE_SHA', 'OVERRIDE_BASE_COMMIT', 'FORCE_BASELINE_REF', 'BASECOMMIT',
285
+ ]) {
286
+ withEnvVar(name, 'not-an-override', () => {
287
+ assert.throws(() => cf.assertBaselineUsable(), (error) => error.code === 'authority_absent', `${name} must never be treated as an override`);
288
+ });
289
+ }
290
+ });
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // C1 ChangeProposal shape / R1.1 / R1.2
294
+ // ---------------------------------------------------------------------------
295
+
296
+ test('C1/R1.1/R1.2: benchmark_remediation is refused for a non-framework_regression class without blocking a distinct authorized_evolution', () => {
297
+ withTempRepo((root) => {
298
+ initRepo(root);
299
+ const inst = cf.createChangeFirewall({ root });
300
+ const changedPaths = ['packages/spec/scripts/x.mjs'];
301
+ assert.throws(
302
+ () => inst.assertHotspotChangeAllowed({
303
+ changedPaths,
304
+ proposal: {
305
+ change_intent: 'benchmark_remediation', proposal_id: 'p', invariant_id: 'inv',
306
+ hypothesis: 'h', owner_layer: 'benchmark_controller', primary_failure_class: 'model_runtime_error',
307
+ paths: changedPaths, failure_ids: ['F1'], evidence: ['e'], true_positive_family: ['tp'],
308
+ negative_control_family: ['nc'], rollback_condition: 'revert',
309
+ },
310
+ }),
311
+ (error) => error.code === 'non_framework_regression',
312
+ );
313
+ // R1.2: this refusal is scoped to benchmark_remediation only; it must not read as
314
+ // "no product change to a protected path is ever possible" -- proven by R1.7 tests below.
315
+ });
316
+ });
317
+
318
+ test('C1: neither variant may embed a release-candidate lifecycle binding digest', () => {
319
+ withTempRepo((root) => {
320
+ initRepo(root);
321
+ const inst = cf.createChangeFirewall({ root });
322
+ assert.throws(
323
+ () => inst.assertHotspotChangeAllowed({
324
+ changedPaths: ['packages/spec/scripts/x.mjs'],
325
+ proposal: {
326
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: 'specs/whatever',
327
+ spec_semantic_digest: cf.sha256Tag('a'), independent_pass_receipt_digest: cf.sha256Tag('b'),
328
+ planned_write_set: ['packages/spec/scripts/x.mjs'], rollback_condition: 'r',
329
+ negative_control_family: ['nc'], treeDigest: cf.sha256Tag('c'),
330
+ },
331
+ }),
332
+ (error) => error.code === 'malformed_proposal',
333
+ );
334
+ });
335
+ });
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // C10 / R1.3 / R1.4: load rules, empty-path rule conditioned on authority state
339
+ // ---------------------------------------------------------------------------
340
+
341
+ test('C10/R1.3: absent authority fails closed for every command except bootstrapBaseline', () => {
342
+ withTempRepo((root) => {
343
+ initRepo(root);
344
+ const inst = cf.createChangeFirewall({ root });
345
+ assert.deepEqual(inst.loadBaselineAuthority(), { state: 'absent' });
346
+ assert.throws(() => inst.assertBaselineUsable(), (error) => error.code === 'authority_absent');
347
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'authority_absent');
348
+ });
349
+ });
350
+
351
+ test('C10/R1.5: computeCandidateDigest binds exactly the C9 field set, non-circular', () => {
352
+ const fields = {
353
+ baselineAuthorityDigest: cf.sha256Tag('a'), baselineGeneration: 0, baselineCommit: '0'.repeat(40),
354
+ failureLedgerDigest: cf.NO_FAILURES_ATTESTATION_DIGEST, failureLedgerGeneration: null,
355
+ remediatedFailureIds: [], changedPaths: ['a'], treeDigest: cf.sha256Tag('t'),
356
+ proposalDigest: cf.NO_CHANGE_ATTESTATION_DIGEST, packageVersion: '1.0.0',
357
+ };
358
+ const digest = cf.computeCandidateDigest(fields);
359
+ assert.match(digest, /^sha256:[0-9a-f]{64}$/);
360
+ assert.throws(() => cf.computeCandidateDigest({ ...fields, candidateDigest: digest }), (error) => error.code === 'malformed_input');
361
+ assert.throws(() => cf.computeCandidateDigest({ ...fields, extra: 1 }), (error) => error.code === 'malformed_input');
362
+ const missing = { ...fields };
363
+ delete missing.treeDigest;
364
+ assert.throws(() => cf.computeCandidateDigest(missing), (error) => error.code === 'malformed_input');
365
+ });
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // C14 BootstrapAttestation / R1.8
369
+ // ---------------------------------------------------------------------------
370
+
371
+ test('C14/R1.8: bootstrapBaseline refuses before the seed PASS exists, and refuses base/head mismatches', () => {
372
+ withTempRepo((root) => {
373
+ const baseCommit = initRepo(root);
374
+ writeLegacyBridge(root, { baseCommit });
375
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/change.cjs'), 'module.exports = {};\n');
376
+ git(root, ['add', '-A']);
377
+ git(root, ['commit', '-qm', 'boot window change']);
378
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
379
+ const inst = cf.createChangeFirewall({ root });
380
+
381
+ // feature spec.json exists but carries no semantic_review_history yet (no seed PASS).
382
+ writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), { validation: {} });
383
+ assert.throws(
384
+ () => inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: cf.sha256Tag('x'), packageVersion: '0.0.1-fixture' }),
385
+ (error) => error.code === 'seed_pass_missing',
386
+ );
387
+
388
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
389
+
390
+ // baseCommit mismatch vs bridge's own bootWindowBaseCommit
391
+ assert.throws(
392
+ () => inst.bootstrapBaseline({ baseCommit: headCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' }),
393
+ (error) => error.code === 'base_commit_mismatch',
394
+ );
395
+
396
+ // stale headCommit (not the actual current HEAD)
397
+ assert.throws(
398
+ () => inst.bootstrapBaseline({ baseCommit, headCommit: baseCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' }),
399
+ (error) => error.code === 'stale_attestation',
400
+ );
401
+
402
+ // bootstrapReviewDigest not bound to the seed PASS entry
403
+ assert.throws(
404
+ () => inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: cf.sha256Tag('wrong'), packageVersion: '0.0.1-fixture' }),
405
+ (error) => error.code === 'stale_attestation',
406
+ );
407
+
408
+ // valid attestation: generation 0 written, baselineCommit explicit
409
+ const boot = inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
410
+ assert.deepEqual(boot.protectedPaths, ['packages/spec/src/claude/scripts/change.cjs']);
411
+ const authority = inst.loadBaselineAuthority();
412
+ assert.equal(authority.state, 'bootstrap');
413
+ assert.equal(authority.record.generation, 0);
414
+ assert.equal(authority.record.baselineCommit, baseCommit);
415
+ assert.equal(authority.record.previousBaselineDigest, null);
416
+
417
+ // running again once authority exists is refused
418
+ assert.throws(
419
+ () => inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' }),
420
+ (error) => error.code === 'authority_exists',
421
+ );
422
+ });
423
+ });
424
+
425
+ test('C10/R1.3: bootstrap state with empty derived protected paths always exits 2 (never a valid no-change bootstrap)', () => {
426
+ withTempRepo((root) => {
427
+ const commit = initRepo(root);
428
+ // Right after a real bootstrapBaseline call, baselineCommit == baseCommit,
429
+ // which always still shows the boot-window diff as pending -- so a
430
+ // genuinely *empty* derived diff under state=bootstrap can only be
431
+ // constructed directly (nothing committed for this path yet, so C10's
432
+ // one-hop provenance rule trusts it per case (2)) to isolate this exact
433
+ // state-conditioned rule from bootstrapBaseline's own, unrelated
434
+ // non-empty-boot-window-diff precondition.
435
+ writeJson(path.join(root, 'packages/spec/benchmarks/release-baseline.json'), {
436
+ state: 'bootstrap', generation: 0, baselineCommit: commit,
437
+ baselinePackageVersion: '0.0.1-fixture', bootstrapReviewDigest: cf.sha256Tag('seed'),
438
+ previousBaselineDigest: null,
439
+ });
440
+ const inst = cf.createChangeFirewall({ root });
441
+ assert.equal(inst.deriveProtectedChangedPaths(commit).length, 0);
442
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'empty_paths_invalid');
443
+ });
444
+ });
445
+
446
+ test('REGRESSION round3 (critical): a committed bootstrap authority record hand-edited out-of-band (baselineCommit swapped) fails closed on the next normal use, before it can create/advance a freeze', () => {
447
+ withTempRepo((root) => {
448
+ const baseCommit = initRepo(root);
449
+ writeLegacyBridge(root, { baseCommit });
450
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
451
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round3-provenance.cjs'), 'module.exports = {};\n');
452
+ git(root, ['add', '-A']);
453
+ git(root, ['commit', '-qm', 'boot window change']);
454
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
455
+ const inst = cf.createChangeFirewall({ root });
456
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
457
+
458
+ // Commit the genuine bootstrap record itself: HEAD now carries a real,
459
+ // trustworthy state=bootstrap/generation=0/baselineCommit=baseCommit record.
460
+ git(root, ['add', '-A']);
461
+ git(root, ['commit', '-qm', 'bootstrap record committed']);
462
+
463
+ // Out-of-band edit: swap baselineCommit to a different real ancestor
464
+ // (headCommit) while leaving state/generation untouched, exactly as
465
+ // C10/R1.3's own load rule requires a bootstrap record to keep generation
466
+ // 0. This tampered record is then itself landed via a later *ordinary*
467
+ // commit -- the exact reviewed laundering path (not left dangling
468
+ // uncommitted) -- before any firewall operation ever inspects it.
469
+ const authorityPath = path.join(root, 'packages/spec/benchmarks/release-baseline.json');
470
+ const tampered = JSON.parse(fs.readFileSync(authorityPath, 'utf8'));
471
+ assert.equal(tampered.state, 'bootstrap');
472
+ assert.equal(tampered.baselineCommit, baseCommit);
473
+ tampered.baselineCommit = headCommit;
474
+ fs.writeFileSync(authorityPath, `${JSON.stringify(tampered, null, 2)}\n`);
475
+ git(root, ['add', '-A']);
476
+ git(root, ['commit', '-qm', 'tampered bootstrap record landed via an ordinary commit']);
477
+
478
+ // Before this fix, assertBaselineUsable only ran verifyAuthorityProvenance
479
+ // for state === 'released', so a tampered *bootstrap* record sailed
480
+ // through unnoticed and could be laundered into the released chain by a
481
+ // later advanceBaseline. It must now fail closed exactly like a tampered
482
+ // released record already does, even after the tamper itself was committed.
483
+ assert.throws(() => inst.assertBaselineUsable(), (error) => error.code === 'out_of_band_edit');
484
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'out_of_band_edit');
485
+ assert.throws(() => inst.advanceBaseline(Buffer.from('{}')), (error) => error.code === 'out_of_band_edit');
486
+ });
487
+ });
488
+
489
+ // ---------------------------------------------------------------------------
490
+ // Full C9/C10/C12/C15 publication lifecycle (R1.7, R1.9-R1.13, I11-I14, I18)
491
+ // ---------------------------------------------------------------------------
492
+
493
+ test('full lifecycle: bootstrap -> authorized_evolution freeze -> publish -> clean no_protected_change -> benchmark_remediation + ledger -> crash/retry -> resolveFailure self-verification', () => {
494
+ withTempRepo((root) => {
495
+ const baseCommit = initRepo(root);
496
+ writeLegacyBridge(root, { baseCommit });
497
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
498
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/boot-window-change.cjs'), 'module.exports = {};\n');
499
+ git(root, ['add', '-A']);
500
+ git(root, ['commit', '-qm', 'boot window change']);
501
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
502
+
503
+ const inst = cf.createChangeFirewall({ root });
504
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
505
+ const authority0 = inst.loadBaselineAuthority();
506
+ assert.equal(authority0.state, 'bootstrap');
507
+
508
+ // --- Cycle 1: authorized_evolution freeze over the boot-window diff (R1.7) ---
509
+ const evo = buildEvolutionSpecFixture(root, { name: 'cycle1-spec' });
510
+ const changedPaths1 = ['packages/spec/src/claude/scripts/boot-window-change.cjs'];
511
+ const proposal1 = {
512
+ change_intent: 'authorized_evolution', proposal_id: 'p1', spec_ref: evo.specRef,
513
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
514
+ planned_write_set: changedPaths1, rollback_condition: 'revert the commit',
515
+ negative_control_family: ['a write outside planned_write_set is rejected'],
516
+ };
517
+ const freeze1 = inst.createFreezeManifest(proposal1);
518
+ assert.equal(freeze1.changeKind, 'protected_change');
519
+ assert.deepEqual(freeze1.remediatedFailureIds, []);
520
+ assert.deepEqual(freeze1.changedPaths, changedPaths1);
521
+ const verified1 = inst.verifyFreezeManifest();
522
+ assert.equal(verified1.candidateDigest, freeze1.candidateDigest);
523
+
524
+ // I14: a superseded (lower-sequence) citation is refused once a fresh re-PASS exists.
525
+ writeFile(evo.designPath, `${fs.readFileSync(evo.designPath, 'utf8')}\n<!-- a legitimate post-PASS correction -->\n`);
526
+ const rebuiltSpec = JSON.parse(fs.readFileSync(path.join(evo.specDir, 'spec.json'), 'utf8'));
527
+ const redigest = VALIDATOR.computeSemanticDigest21(evo.specDir, rebuiltSpec);
528
+ assert.deepEqual(redigest.errors, []);
529
+ assert.notEqual(redigest.digest, evo.digest, 'the correction must actually change the digest');
530
+ const freshReceiptDigest = cf.sha256Tag('cycle1-spec-independent-pass-2');
531
+ rebuiltSpec.validation.authoring_validation = {
532
+ schema_version: '1',
533
+ requirements: { digest: DIGEST.digestFileBytes(evo.requirementsPath), validated_at: '2026-08-16T00:10:00+07:00' },
534
+ design: { digest: DIGEST.digestFileBytes(evo.designPath), validated_at: '2026-08-16T00:10:00+07:00' },
535
+ research: { digest: DIGEST.RESEARCH_ABSENT_DIGEST, validated_at: '2026-08-16T00:10:00+07:00' },
536
+ tasks: { digest: DIGEST.TASKS_ABSENT_DIGEST, validated_at: '2026-08-16T00:10:00+07:00' },
537
+ };
538
+ rebuiltSpec.validation.semantic_review_history.entries.push({
539
+ sequence: 1, review_epoch: 1, verdict: 'PASS', semantic_digest: redigest.digest, review_receipt_digest: freshReceiptDigest,
540
+ });
541
+ writeJson(path.join(evo.specDir, 'spec.json'), rebuiltSpec);
542
+ assert.throws(
543
+ () => inst.assertHotspotChangeAllowed({
544
+ changedPaths: changedPaths1,
545
+ proposal: { ...proposal1, spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest },
546
+ }),
547
+ (error) => error.code === 'stale_spec_digest',
548
+ );
549
+ const rebound = inst.assertHotspotChangeAllowed({
550
+ changedPaths: changedPaths1,
551
+ proposal: { ...proposal1, spec_semantic_digest: redigest.digest, independent_pass_receipt_digest: freshReceiptDigest },
552
+ });
553
+ assert.equal(rebound.changeKind, 'protected_change');
554
+ assert.throws(
555
+ () => inst.assertHotspotChangeAllowed({
556
+ changedPaths: changedPaths1,
557
+ proposal: { ...proposal1, spec_semantic_digest: redigest.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest },
558
+ }),
559
+ (error) => error.code === 'superseded_pass_citation',
560
+ );
561
+
562
+ // Build + publish receipt 1 (I11/I13/I12).
563
+ const freezeBytes1 = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
564
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-artifact-1.tgz'), 'fake-tgz-bytes-1');
565
+ const artifactDigest1 = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/release-artifact-1.tgz'));
566
+ const receiptObj1 = {
567
+ schema_version: '1', receipt_id: 'r1', status: 'published',
568
+ baselineAuthorityDigest: authority0.digest, baselineGeneration: authority0.record.generation,
569
+ freezeDigest: cf.sha256Tag(freezeBytes1), candidateDigest: freeze1.candidateDigest,
570
+ treeDigest: freeze1.treeDigest, packageVersion: freeze1.packageVersion,
571
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/release-artifact-1.tgz',
572
+ artifactDigest: artifactDigest1, resolvedFailureIds: [],
573
+ };
574
+ const receiptBytes1 = Buffer.from(`${JSON.stringify(receiptObj1, null, 2)}\n`);
575
+ const publish1 = inst.advanceBaseline(receiptBytes1);
576
+ assert.equal(publish1.status, 'published');
577
+ const afterPublish1 = inst.loadBaselineAuthority();
578
+ assert.equal(afterPublish1.state, 'released');
579
+ assert.equal(afterPublish1.record.generation, 1);
580
+ assert.equal(afterPublish1.record.releaseReceiptDigest, cf.sha256Tag(receiptBytes1));
581
+ assert.equal(afterPublish1.record.baselineCommit, headCommit);
582
+ assert.equal(fs.existsSync(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json')), false, 'advanceBaseline must invalidate the freeze');
583
+
584
+ // I11 (1a): idempotent replay against intact canonical evidence is a no-op.
585
+ const republish1 = inst.advanceBaseline(receiptBytes1);
586
+ assert.equal(republish1.status, 'already_published');
587
+ assert.equal(inst.loadBaselineAuthority().record.generation, 1);
588
+
589
+ // I11 (1b): idempotent repair when canonical evidence is lost/corrupted.
590
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-receipt.json'), 'corrupted-not-json');
591
+
592
+ // Prove the implementation itself re-reads and re-verifies the repair
593
+ // write rather than trusting atomicWriteBytes blindly: seam the sole
594
+ // physical write call the repair path uses so the bytes actually landing
595
+ // on disk are NOT the supplied receipt bytes, then assert advanceBaseline
596
+ // refuses to report "repaired" on that unverified write.
597
+ const realWriteFileSync = fs.writeFileSync;
598
+ fs.writeFileSync = (fd, data, ...rest) => {
599
+ if (Buffer.isBuffer(data) && data.equals(receiptBytes1)) {
600
+ return realWriteFileSync.call(fs, fd, Buffer.from('corrupted-by-seam'), ...rest);
601
+ }
602
+ return realWriteFileSync.call(fs, fd, data, ...rest);
603
+ };
604
+ try {
605
+ assert.throws(
606
+ () => inst.advanceBaseline(receiptBytes1),
607
+ (error) => error.code === 'receipt_unverifiable',
608
+ );
609
+ } finally {
610
+ fs.writeFileSync = realWriteFileSync;
611
+ }
612
+ // The failed repair attempt must never rotate authority or claim success.
613
+ assert.equal(inst.loadBaselineAuthority().record.generation, 1);
614
+ assert.equal(inst.loadBaselineAuthority().record.releaseReceiptDigest, cf.sha256Tag(receiptBytes1));
615
+
616
+ // With the seam removed, a real retry idempotently repairs the canonical
617
+ // evidence and its own re-verification confirms the write before success.
618
+ const repaired = inst.advanceBaseline(receiptBytes1);
619
+ assert.equal(repaired.status, 'repaired');
620
+ assert.deepEqual(readFileBytes(root, 'packages/spec/.cafekit-release/release-receipt.json'), receiptBytes1);
621
+
622
+ // R1.3/D1: released state with empty derived protected paths is a clean no_protected_change freeze.
623
+ const cleanFreeze = inst.createFreezeManifest(null);
624
+ assert.equal(cleanFreeze.changeKind, 'no_protected_change');
625
+ assert.deepEqual(cleanFreeze.changedPaths, []);
626
+ assert.equal(cleanFreeze.proposalDigest, cf.NO_CHANGE_ATTESTATION_DIGEST);
627
+ inst.verifyFreezeManifest();
628
+ inst.invalidateFreeze();
629
+
630
+ // --- Cycle 2: benchmark_remediation + C15 ledger + reclassify + crash resume + resolveFailure ---
631
+ writeFile(path.join(root, 'packages/spec/scripts/foo2.mjs'), 'export const x = 2;\n');
632
+ git(root, ['add', '-A']);
633
+ git(root, ['commit', '-qm', 'cycle2 change']);
634
+ const headCommit2 = git(root, ['rev-parse', 'HEAD']).trim();
635
+ const changedPaths2 = ['packages/spec/scripts/foo2.mjs'];
636
+
637
+ inst.openFailure({
638
+ failure_id: 'F1', paths: changedPaths2, candidate_digest: cf.sha256Tag('candidate-1'),
639
+ evidence_digest: cf.sha256Tag('evidence-1'), occurred_at: new Date().toISOString(),
640
+ });
641
+ inst.classifyFailure({
642
+ failure_id: 'F1', primary_failure_class: 'model_runtime_error',
643
+ adjudication_digest: cf.sha256Tag('adj-1'), occurred_at: new Date().toISOString(),
644
+ });
645
+
646
+ // I18: reclassify refused on mismatched previous class / reused digest / while unresolved is fine, resolved-after is not yet reached.
647
+ assert.throws(
648
+ () => inst.reclassifyFailure({
649
+ failure_id: 'F1', previous_primary_failure_class: 'oracle_defect', primary_failure_class: 'framework_regression',
650
+ adjudication_digest: cf.sha256Tag('adj-2'), occurred_at: new Date().toISOString(),
651
+ }),
652
+ (error) => error.code === 'stale_previous_class',
653
+ );
654
+ assert.throws(
655
+ () => inst.reclassifyFailure({
656
+ failure_id: 'F1', previous_primary_failure_class: 'model_runtime_error', primary_failure_class: 'framework_regression',
657
+ adjudication_digest: cf.sha256Tag('adj-1'), occurred_at: new Date().toISOString(),
658
+ }),
659
+ (error) => error.code === 'reused_adjudication_digest',
660
+ );
661
+ inst.reclassifyFailure({
662
+ failure_id: 'F1', previous_primary_failure_class: 'model_runtime_error', primary_failure_class: 'framework_regression',
663
+ adjudication_digest: cf.sha256Tag('adj-2'), occurred_at: new Date().toISOString(),
664
+ });
665
+ assert.equal(inst.deriveFailureState('F1').currentClass, 'framework_regression');
666
+ assert.equal(inst.deriveFailureState('F1').blocking, true);
667
+
668
+ // I8: authorized_evolution is refused unconditionally when it intersects this open entry.
669
+ assert.throws(
670
+ () => inst.assertHotspotChangeAllowed({
671
+ changedPaths: changedPaths2,
672
+ proposal: {
673
+ change_intent: 'authorized_evolution', proposal_id: 'p2', spec_ref: evo.specRef,
674
+ spec_semantic_digest: redigest.digest, independent_pass_receipt_digest: freshReceiptDigest,
675
+ planned_write_set: changedPaths2, rollback_condition: 'r', negative_control_family: ['nc'],
676
+ },
677
+ }),
678
+ (error) => error.code === 'open_ledger_intersection',
679
+ );
680
+
681
+ const proposal2 = {
682
+ change_intent: 'benchmark_remediation', proposal_id: 'p2', invariant_id: 'INV-1',
683
+ hypothesis: 'foo2 regressed', owner_layer: 'benchmark_controller', primary_failure_class: 'framework_regression',
684
+ paths: changedPaths2, failure_ids: ['F1'], evidence: ['log line'],
685
+ true_positive_family: ['case-a'], negative_control_family: ['case-b'], rollback_condition: 'revert foo2.mjs',
686
+ };
687
+ const freeze2 = inst.createFreezeManifest(proposal2);
688
+ assert.deepEqual(freeze2.remediatedFailureIds, ['F1']);
689
+
690
+ const freezeBytes2 = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
691
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-artifact-2.tgz'), 'fake-tgz-bytes-2');
692
+ const artifactDigest2 = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/release-artifact-2.tgz'));
693
+ const receiptObj2 = {
694
+ schema_version: '1', receipt_id: 'r2', status: 'published',
695
+ baselineAuthorityDigest: afterPublish1.digest, baselineGeneration: afterPublish1.record.generation,
696
+ freezeDigest: cf.sha256Tag(freezeBytes2), candidateDigest: freeze2.candidateDigest,
697
+ treeDigest: freeze2.treeDigest, packageVersion: freeze2.packageVersion,
698
+ releaseCommit: headCommit2, artifactPath: 'packages/spec/.cafekit-release/release-artifact-2.tgz',
699
+ artifactDigest: artifactDigest2, resolvedFailureIds: ['F1'],
700
+ };
701
+ const receiptBytes2 = Buffer.from(`${JSON.stringify(receiptObj2, null, 2)}\n`);
702
+
703
+ // I11: crash-after-event-A-before-event-B -- pre-stage the receipt bytes at the
704
+ // canonical path exactly as a prior crashed call would have left them, then
705
+ // prove a fresh advanceBaseline call still resumes correctly through event B.
706
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-receipt.json'), receiptBytes2);
707
+ assert.equal(inst.loadBaselineAuthority().record.generation, 1, 'authority must still be unrotated at this staged point');
708
+ const publish2 = inst.advanceBaseline(receiptBytes2);
709
+ assert.equal(publish2.status, 'published');
710
+ const afterPublish2 = inst.loadBaselineAuthority();
711
+ assert.equal(afterPublish2.state, 'released');
712
+ assert.equal(afterPublish2.record.generation, 2);
713
+ assert.equal(afterPublish2.record.releaseReceiptDigest, cf.sha256Tag(receiptBytes2));
714
+
715
+ // I11: crash-between-event-B-and-event-C -- retry advanceBaseline (idempotent
716
+ // no-op) before ever calling resolveFailure; ledger must still show F1 blocking.
717
+ assert.equal(inst.deriveFailureState('F1').blocking, true);
718
+ const resumeAdvance = inst.advanceBaseline(receiptBytes2);
719
+ assert.equal(resumeAdvance.status, 'already_published');
720
+
721
+ // I13/C15: resolveFailure independently re-verifies the persisted receipt and
722
+ // id membership before appending -- never trusting a caller-supplied claim.
723
+ const resolved = inst.resolveFailure({ failure_id: 'F1', decision_digest: cf.sha256Tag('decision-1') });
724
+ assert.equal(resolved.status, 'resolved');
725
+ assert.equal(inst.deriveFailureState('F1').blocking, false);
726
+ assert.equal(inst.deriveFailureState('F1').resolved, true);
727
+
728
+ // per-id idempotency: matching replay is a no-op.
729
+ const replaySame = inst.resolveFailure({ failure_id: 'F1', decision_digest: cf.sha256Tag('decision-1') });
730
+ assert.equal(replaySame.status, 'noop');
731
+ });
732
+ });
733
+
734
+ test('REGRESSION (critical): advanceBaseline event (A) physically corrupted-by-seam on a genuinely new first publish fails closed via read-after-write verification -- event (B) never runs and authority never rotates', () => {
735
+ withTempRepo((root) => {
736
+ const baseCommit = initRepo(root);
737
+ writeLegacyBridge(root, { baseCommit });
738
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
739
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/event-a-corrupt-change.cjs'), 'module.exports = {};\n');
740
+ git(root, ['add', '-A']);
741
+ git(root, ['commit', '-qm', 'boot window change']);
742
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
743
+
744
+ const inst = cf.createChangeFirewall({ root });
745
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
746
+ const authority0 = inst.loadBaselineAuthority();
747
+ assert.equal(authority0.state, 'bootstrap');
748
+
749
+ const evo = buildEvolutionSpecFixture(root, { name: 'event-a-corrupt-spec' });
750
+ const changedPaths = ['packages/spec/src/claude/scripts/event-a-corrupt-change.cjs'];
751
+ const proposal = {
752
+ change_intent: 'authorized_evolution', proposal_id: 'p1', spec_ref: evo.specRef,
753
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
754
+ planned_write_set: changedPaths, rollback_condition: 'revert the commit',
755
+ negative_control_family: ['a write outside planned_write_set is rejected'],
756
+ };
757
+ const freeze = inst.createFreezeManifest(proposal);
758
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
759
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/event-a-corrupt-artifact.tgz'), 'fake-tgz-bytes');
760
+ const artifactDigest = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/event-a-corrupt-artifact.tgz'));
761
+ const receiptObj = {
762
+ schema_version: '1', receipt_id: 'r-event-a-corrupt', status: 'published',
763
+ baselineAuthorityDigest: authority0.digest, baselineGeneration: authority0.record.generation,
764
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest,
765
+ treeDigest: freeze.treeDigest, packageVersion: freeze.packageVersion,
766
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/event-a-corrupt-artifact.tgz',
767
+ artifactDigest, resolvedFailureIds: [],
768
+ };
769
+ const receiptBytes = Buffer.from(`${JSON.stringify(receiptObj, null, 2)}\n`);
770
+
771
+ const authorityPath = path.join(root, 'packages/spec/benchmarks/release-baseline.json');
772
+ const baselineBytesBefore = fs.readFileSync(authorityPath);
773
+
774
+ // Seam the sole physical write call event (A) uses so the bytes that
775
+ // actually land on disk are silently corrupted -- proves advanceBaseline
776
+ // re-reads and re-verifies the canonical file it just wrote on a
777
+ // genuinely new first publish too, not only on the (1b) repair branch.
778
+ const realWriteFileSync = fs.writeFileSync;
779
+ fs.writeFileSync = (fd, data, ...rest) => {
780
+ if (Buffer.isBuffer(data) && data.equals(receiptBytes)) {
781
+ return realWriteFileSync.call(fs, fd, Buffer.from('corrupted-event-a-by-seam'), ...rest);
782
+ }
783
+ return realWriteFileSync.call(fs, fd, data, ...rest);
784
+ };
785
+ try {
786
+ assert.throws(
787
+ () => inst.advanceBaseline(receiptBytes),
788
+ (error) => error.code === 'receipt_unverifiable',
789
+ );
790
+ } finally {
791
+ fs.writeFileSync = realWriteFileSync;
792
+ }
793
+
794
+ // Event (B) must never have run: authority stays exactly bootstrap generation 0,
795
+ // byte-identical to its pre-call state, and the call never reports published.
796
+ const authorityAfter = inst.loadBaselineAuthority();
797
+ assert.equal(authorityAfter.state, 'bootstrap');
798
+ assert.equal(authorityAfter.record.generation, 0);
799
+ assert.deepEqual(fs.readFileSync(authorityPath), baselineBytesBefore, 'release-baseline.json bytes must remain exactly as they were before the corrupted first-publish attempt');
800
+ });
801
+ });
802
+
803
+ test('I13: resolveFailure refuses a mismatched-digest replay for an already-resolved id, never coercing it', () => {
804
+ withTempRepo((root) => {
805
+ initRepo(root);
806
+ const inst = cf.createChangeFirewall({ root });
807
+ // Hand-constructed authority/receipt pair (nothing committed for this path
808
+ // yet, so C10's one-hop provenance rule trusts it per case (2)): isolates
809
+ // resolveFailure's own mismatched-replay defense (I13) from the full gate
810
+ // chain, which independently prevents this exact scenario from ever
811
+ // arising through the normal pipeline (I12).
812
+ inst.openFailure({
813
+ failure_id: 'F1', paths: ['packages/spec/scripts/a.mjs'], candidate_digest: cf.sha256Tag('c'),
814
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
815
+ });
816
+ inst.classifyFailure({
817
+ failure_id: 'F1', primary_failure_class: 'framework_regression',
818
+ adjudication_digest: cf.sha256Tag('adj'), occurred_at: new Date().toISOString(),
819
+ });
820
+
821
+ const receiptA = {
822
+ schema_version: '1', receipt_id: 'rA', status: 'published',
823
+ baselineAuthorityDigest: cf.sha256Tag('authA'), baselineGeneration: 1,
824
+ freezeDigest: cf.sha256Tag('freezeA'), candidateDigest: cf.sha256Tag('candA'),
825
+ treeDigest: cf.sha256Tag('treeA'), packageVersion: '0.0.1-fixture',
826
+ releaseCommit: '1'.repeat(40), artifactPath: 'packages/spec/.cafekit-release/a.tgz',
827
+ artifactDigest: cf.sha256Tag('artA'), resolvedFailureIds: ['F1'],
828
+ };
829
+ const receiptBytesA = Buffer.from(`${JSON.stringify(receiptA, null, 2)}\n`);
830
+ writeJson(path.join(root, 'packages/spec/benchmarks/release-baseline.json'), {
831
+ state: 'released', generation: 1, baselineCommit: '1'.repeat(40),
832
+ baselinePackageVersion: '0.0.1-fixture', releaseReceiptDigest: cf.sha256Tag(receiptBytesA),
833
+ previousBaselineDigest: cf.sha256Tag('genesis'),
834
+ });
835
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-receipt.json'), receiptBytesA);
836
+ inst.resolveFailure({ failure_id: 'F1', decision_digest: cf.sha256Tag('decisionA') });
837
+ assert.equal(inst.deriveFailureState('F1').resolved, true);
838
+
839
+ // Rotate authority to a different published receipt that also names F1.
840
+ const previousAuthorityBytes = readFileBytes(root, 'packages/spec/benchmarks/release-baseline.json');
841
+ const receiptB = { ...receiptA, receipt_id: 'rB', releaseCommit: '2'.repeat(40) };
842
+ const receiptBytesB = Buffer.from(`${JSON.stringify(receiptB, null, 2)}\n`);
843
+ writeJson(path.join(root, 'packages/spec/benchmarks/release-baseline.json'), {
844
+ state: 'released', generation: 2, baselineCommit: '2'.repeat(40),
845
+ baselinePackageVersion: '0.0.1-fixture', releaseReceiptDigest: cf.sha256Tag(receiptBytesB),
846
+ previousBaselineDigest: cf.sha256Tag(previousAuthorityBytes),
847
+ });
848
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-receipt.json'), receiptBytesB);
849
+
850
+ assert.throws(
851
+ () => inst.resolveFailure({ failure_id: 'F1', decision_digest: cf.sha256Tag('decisionB') }),
852
+ (error) => error.code === 'resolve_conflict',
853
+ );
854
+ });
855
+ });
856
+
857
+ // ---------------------------------------------------------------------------
858
+ // C15/R1.9/R1.10: no-bypass on malformed ledger, freeze staleness on ledger events
859
+ // ---------------------------------------------------------------------------
860
+
861
+ test('C15/R1.9: loadFailureLedger/assertFailureLedgerUsable fail closed on a malformed or out-of-band-edited ledger, with no bypass', () => {
862
+ withTempRepo((root) => {
863
+ initRepo(root);
864
+ const inst = cf.createChangeFirewall({ root });
865
+ inst.openFailure({
866
+ failure_id: 'F1', paths: ['packages/spec/scripts/a.mjs'], candidate_digest: cf.sha256Tag('c'),
867
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
868
+ });
869
+ git(root, ['add', '-A']);
870
+ git(root, ['commit', '-qm', 'ledger opened F1']);
871
+
872
+ // out-of-band edit: content diverges from the committed generation without advancing it.
873
+ const ledgerPath = path.join(root, 'packages/spec/benchmarks/benchmark-failure-ledger.json');
874
+ const tampered = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
875
+ tampered.events[0].paths = ['packages/spec/scripts/tampered.mjs'];
876
+ fs.writeFileSync(ledgerPath, `${JSON.stringify(tampered, null, 2)}\n`);
877
+ assert.throws(() => inst.assertFailureLedgerUsable(), (error) => error.code === 'out_of_band_edit');
878
+
879
+ // malformed shape (unknown event type) fails closed regardless of provenance.
880
+ const malformed = JSON.parse(readFileBytes(root, 'packages/spec/benchmarks/benchmark-failure-ledger.json').toString('utf8'));
881
+ malformed.events[0].type = 'unknown_event_type';
882
+ fs.writeFileSync(ledgerPath, `${JSON.stringify(malformed, null, 2)}\n`);
883
+ assert.throws(() => inst.loadFailureLedger(), (error) => error.code === 'malformed_ledger');
884
+ });
885
+ });
886
+
887
+ test('C9/R1.10: any accepted C15 ledger event since the last freeze makes that freeze stale', () => {
888
+ withTempRepo((root) => {
889
+ const baseCommit = initRepo(root);
890
+ writeLegacyBridge(root, { baseCommit });
891
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
892
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/x.cjs'), 'module.exports = {};\n');
893
+ git(root, ['add', '-A']);
894
+ git(root, ['commit', '-qm', 'boot window change']);
895
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
896
+ const inst = cf.createChangeFirewall({ root });
897
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
898
+
899
+ const evo = buildEvolutionSpecFixture(root, { name: 'staleness-spec' });
900
+ const changedPaths = ['packages/spec/src/claude/scripts/x.cjs'];
901
+ inst.createFreezeManifest({
902
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
903
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
904
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
905
+ });
906
+ inst.verifyFreezeManifest();
907
+
908
+ inst.openFailure({
909
+ failure_id: 'F-stale', paths: ['packages/spec/scripts/unrelated.mjs'], candidate_digest: cf.sha256Tag('c'),
910
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
911
+ });
912
+ assert.throws(() => inst.verifyFreezeManifest(), (error) => error.code === 'freeze_stale');
913
+ });
914
+ });
915
+
916
+ // ---------------------------------------------------------------------------
917
+ // I13: resolveFailure's independent self-verification, isolated unit coverage
918
+ // ---------------------------------------------------------------------------
919
+
920
+ test('I13: resolveFailure refuses an id absent from the persisted receipt, a digest mismatch, and a not-released authority', () => {
921
+ withTempRepo((root) => {
922
+ const baseCommit = initRepo(root);
923
+ writeLegacyBridge(root, { baseCommit });
924
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
925
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/y.cjs'), 'module.exports = {};\n');
926
+ git(root, ['add', '-A']);
927
+ git(root, ['commit', '-qm', 'boot window change']);
928
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
929
+ const inst = cf.createChangeFirewall({ root });
930
+
931
+ // not-released authority (still bootstrap state)
932
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
933
+ assert.throws(
934
+ () => inst.resolveFailure({ failure_id: 'F1', decision_digest: cf.sha256Tag('d') }),
935
+ (error) => error.code === 'not_released',
936
+ );
937
+
938
+ const evo = buildEvolutionSpecFixture(root, { name: 'resolve-spec' });
939
+ const changedPaths = ['packages/spec/src/claude/scripts/y.cjs'];
940
+ const freeze = inst.createFreezeManifest({
941
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
942
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
943
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
944
+ });
945
+ const authorityBefore = inst.loadBaselineAuthority();
946
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
947
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz'), 'bytes');
948
+ const artifactDigest = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/artifact.tgz'));
949
+ const receiptObj = {
950
+ schema_version: '1', receipt_id: 'r', status: 'published',
951
+ baselineAuthorityDigest: authorityBefore.digest, baselineGeneration: authorityBefore.record.generation,
952
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest, treeDigest: freeze.treeDigest,
953
+ packageVersion: freeze.packageVersion, releaseCommit: headCommit,
954
+ artifactPath: 'packages/spec/.cafekit-release/artifact.tgz', artifactDigest, resolvedFailureIds: [],
955
+ };
956
+ const receiptBytes = Buffer.from(`${JSON.stringify(receiptObj, null, 2)}\n`);
957
+ inst.advanceBaseline(receiptBytes);
958
+
959
+ // id absent from resolvedFailureIds ([]).
960
+ assert.throws(
961
+ () => inst.resolveFailure({ failure_id: 'GHOST', decision_digest: cf.sha256Tag('d') }),
962
+ (error) => error.code === 'not_in_receipt',
963
+ );
964
+
965
+ // canonical receipt corrupted (unreadable bytes): no usable evidence to verify against.
966
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-receipt.json'), 'not-json-at-all');
967
+ assert.throws(
968
+ () => inst.resolveFailure({ failure_id: 'GHOST', decision_digest: cf.sha256Tag('d') }),
969
+ (error) => error.code === 'receipt_missing',
970
+ );
971
+ });
972
+ });
973
+
974
+ // ---------------------------------------------------------------------------
975
+ // Independent-review regressions (four blockers)
976
+ // ---------------------------------------------------------------------------
977
+
978
+ test('REGRESSION (critical): verifyFreezeManifest fails closed when a protected path is added or removed after freeze creation', () => {
979
+ withTempRepo((root) => {
980
+ const baseCommit = initRepo(root);
981
+ writeLegacyBridge(root, { baseCommit });
982
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
983
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/frozen-change.cjs'), 'module.exports = {};\n');
984
+ git(root, ['add', '-A']);
985
+ git(root, ['commit', '-qm', 'boot window change']);
986
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
987
+ const inst = cf.createChangeFirewall({ root });
988
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
989
+
990
+ const evo = buildEvolutionSpecFixture(root, { name: 'freeze-gap-spec' });
991
+ const changedPaths = ['packages/spec/src/claude/scripts/frozen-change.cjs'];
992
+ inst.createFreezeManifest({
993
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
994
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
995
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
996
+ });
997
+ inst.verifyFreezeManifest(); // sanity: valid immediately after creation
998
+
999
+ // A protected path is ADDED after the freeze was created -- the recorded
1000
+ // changedPaths list alone (re-hashed by treeDigest) would never notice this;
1001
+ // only a fresh re-derivation against the current working tree catches it.
1002
+ writeFile(path.join(root, 'packages/spec/scripts/added-after-freeze.mjs'), 'export const x = 1;\n');
1003
+ assert.throws(() => inst.verifyFreezeManifest(), (error) => error.code === 'freeze_stale');
1004
+ fs.rmSync(path.join(root, 'packages/spec/scripts/added-after-freeze.mjs'));
1005
+ inst.verifyFreezeManifest(); // back to valid once the extra path is gone
1006
+
1007
+ // The frozen change is REMOVED after the freeze was created (working tree
1008
+ // reverts to matching baselineCommit for that path) -- derived paths
1009
+ // become empty, no longer matching the frozen non-empty set.
1010
+ fs.rmSync(path.join(root, 'packages/spec/src/claude/scripts/frozen-change.cjs'));
1011
+ assert.throws(() => inst.verifyFreezeManifest(), (error) => error.code === 'freeze_stale');
1012
+ });
1013
+ });
1014
+
1015
+ test('REGRESSION (high): advanceBaseline rejects a path-traversal, absolute, or symlinked artifactPath -- never a naive string-prefix check', () => {
1016
+ withTempRepo((root) => {
1017
+ const baseCommit = initRepo(root);
1018
+ writeLegacyBridge(root, { baseCommit });
1019
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1020
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/artifact-path-change.cjs'), 'module.exports = {};\n');
1021
+ git(root, ['add', '-A']);
1022
+ git(root, ['commit', '-qm', 'boot window change']);
1023
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1024
+ const inst = cf.createChangeFirewall({ root });
1025
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1026
+
1027
+ const evo = buildEvolutionSpecFixture(root, { name: 'artifact-path-spec' });
1028
+ const changedPaths = ['packages/spec/src/claude/scripts/artifact-path-change.cjs'];
1029
+ const freeze = inst.createFreezeManifest({
1030
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
1031
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
1032
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1033
+ });
1034
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1035
+ const authority = inst.loadBaselineAuthority();
1036
+
1037
+ function buildReceipt(artifactPath, artifactDigest) {
1038
+ return {
1039
+ schema_version: '1', receipt_id: 'r', status: 'published',
1040
+ baselineAuthorityDigest: authority.digest, baselineGeneration: authority.record.generation,
1041
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest,
1042
+ treeDigest: freeze.treeDigest, packageVersion: freeze.packageVersion,
1043
+ releaseCommit: headCommit, artifactPath, artifactDigest, resolvedFailureIds: [],
1044
+ };
1045
+ }
1046
+ function receiptBytesFor(artifactPath, artifactDigest) {
1047
+ return Buffer.from(`${JSON.stringify(buildReceipt(artifactPath, artifactDigest), null, 2)}\n`);
1048
+ }
1049
+
1050
+ // Lexical `..` traversal: the raw string still starts with the reserved
1051
+ // prefix as TEXT, but resolves outside the project root once `..`
1052
+ // segments are followed -- a naive .startsWith() check would accept this.
1053
+ assert.throws(
1054
+ () => inst.advanceBaseline(receiptBytesFor('packages/spec/.cafekit-release/../../../../../../../etc/passwd', cf.sha256Tag('whatever'))),
1055
+ (error) => error.code === 'artifact_path_escape',
1056
+ );
1057
+
1058
+ // Absolute path.
1059
+ assert.throws(
1060
+ () => inst.advanceBaseline(receiptBytesFor('/etc/passwd', cf.sha256Tag('whatever'))),
1061
+ (error) => error.code === 'artifact_path_escape',
1062
+ );
1063
+
1064
+ const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-artifact-outside-'));
1065
+ try {
1066
+ const realOutsideFile = path.join(outsideDir, 'real.tgz');
1067
+ fs.writeFileSync(realOutsideFile, 'real-bytes');
1068
+ const realDigest = cf.sha256Tag(fs.readFileSync(realOutsideFile));
1069
+
1070
+ // Symlink at the final path component itself.
1071
+ fs.symlinkSync(realOutsideFile, path.join(root, 'packages/spec/.cafekit-release/linked-artifact.tgz'));
1072
+ assert.throws(
1073
+ () => inst.advanceBaseline(receiptBytesFor('packages/spec/.cafekit-release/linked-artifact.tgz', realDigest)),
1074
+ (error) => error.code === 'artifact_invalid',
1075
+ );
1076
+
1077
+ // Symlinked ANCESTOR directory (the final component is a real file at
1078
+ // the resolved target, so lstat on it alone would not catch this).
1079
+ fs.symlinkSync(outsideDir, path.join(root, 'packages/spec/.cafekit-release/linked-dir'), 'dir');
1080
+ assert.throws(
1081
+ () => inst.advanceBaseline(receiptBytesFor('packages/spec/.cafekit-release/linked-dir/real.tgz', realDigest)),
1082
+ (error) => error.code === 'artifact_invalid',
1083
+ );
1084
+ } finally {
1085
+ fs.rmSync(outsideDir, { recursive: true, force: true });
1086
+ }
1087
+ });
1088
+ });
1089
+
1090
+ test('REGRESSION (medium): createFreezeManifest/verifyFreezeManifest fail closed when baselineCommit is not an ancestor of current HEAD', () => {
1091
+ withTempRepo((root) => {
1092
+ initRepo(root);
1093
+ const mainBranch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
1094
+ git(root, ['checkout', '-qb', 'divergent-branch']);
1095
+ writeFile(path.join(root, 'divergent.txt'), 'x');
1096
+ git(root, ['add', '-A']);
1097
+ git(root, ['commit', '-qm', 'divergent commit']);
1098
+ const divergentCommit = git(root, ['rev-parse', 'HEAD']).trim();
1099
+ git(root, ['checkout', '-q', mainBranch]);
1100
+
1101
+ writeJson(path.join(root, 'packages/spec/benchmarks/release-baseline.json'), {
1102
+ state: 'bootstrap', generation: 0, baselineCommit: divergentCommit,
1103
+ baselinePackageVersion: '0.0.1-fixture', bootstrapReviewDigest: cf.sha256Tag('seed'),
1104
+ previousBaselineDigest: null,
1105
+ });
1106
+ const inst = cf.createChangeFirewall({ root });
1107
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'baseline_not_ancestor');
1108
+
1109
+ // Hand-place a freeze that is otherwise internally consistent (matching
1110
+ // authority digest/generation), to prove verifyFreezeManifest
1111
+ // independently re-checks ancestry too, never trusting whatever a
1112
+ // (hypothetical) prior create call had accepted.
1113
+ const authority = inst.loadBaselineAuthority();
1114
+ const treeDigest = inst.computeProtectedTreeDigest([]);
1115
+ const candidateFields = {
1116
+ baselineAuthorityDigest: authority.digest, baselineGeneration: 0, baselineCommit: divergentCommit,
1117
+ failureLedgerDigest: cf.NO_FAILURES_ATTESTATION_DIGEST, failureLedgerGeneration: null,
1118
+ remediatedFailureIds: [], changedPaths: [], treeDigest,
1119
+ proposalDigest: cf.NO_CHANGE_ATTESTATION_DIGEST, packageVersion: '0.0.1-fixture',
1120
+ };
1121
+ writeJson(path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json'), {
1122
+ ...candidateFields, changeKind: 'no_protected_change', candidateDigest: cf.computeCandidateDigest(candidateFields),
1123
+ });
1124
+ assert.throws(() => inst.verifyFreezeManifest(), (error) => error.code === 'baseline_not_ancestor');
1125
+ });
1126
+ });
1127
+
1128
+ test('REGRESSION (medium): authorized_evolution refuses a spec_ref reached through a symlinked ancestor directory', () => {
1129
+ withTempRepo((root) => {
1130
+ initRepo(root);
1131
+ const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-specref-outside-'));
1132
+ try {
1133
+ fs.mkdirSync(path.join(outsideDir, 'actual-spec'), { recursive: true });
1134
+ fs.mkdirSync(path.join(root, 'specs'), { recursive: true });
1135
+ fs.symlinkSync(outsideDir, path.join(root, 'specs', 'linked-parent'), 'dir');
1136
+
1137
+ const inst = cf.createChangeFirewall({ root });
1138
+ const proposalFor = (specRef) => ({
1139
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: specRef,
1140
+ spec_semantic_digest: cf.sha256Tag('a'), independent_pass_receipt_digest: cf.sha256Tag('b'),
1141
+ planned_write_set: ['packages/spec/scripts/x.mjs'], rollback_condition: 'r', negative_control_family: ['nc'],
1142
+ });
1143
+
1144
+ // Symlinked ancestor directory: the final resolved target is a real
1145
+ // directory, so a lexical-only or lstat-only check would accept it.
1146
+ assert.throws(
1147
+ () => inst.assertHotspotChangeAllowed({
1148
+ changedPaths: ['packages/spec/scripts/x.mjs'],
1149
+ proposal: proposalFor('specs/linked-parent/actual-spec'),
1150
+ }),
1151
+ (error) => error.code === 'path_escape',
1152
+ );
1153
+
1154
+ // Direct symlink at spec_ref itself (the simpler case) is also refused.
1155
+ fs.symlinkSync(outsideDir, path.join(root, 'specs', 'direct-link'), 'dir');
1156
+ assert.throws(
1157
+ () => inst.assertHotspotChangeAllowed({
1158
+ changedPaths: ['packages/spec/scripts/x.mjs'],
1159
+ proposal: proposalFor('specs/direct-link'),
1160
+ }),
1161
+ (error) => error.code === 'path_escape',
1162
+ );
1163
+ } finally {
1164
+ fs.rmSync(outsideDir, { recursive: true, force: true });
1165
+ }
1166
+ });
1167
+ });
1168
+
1169
+ // ---------------------------------------------------------------------------
1170
+ // Round 2 fresh-review regressions (1 critical, reproduced end-to-end): a
1171
+ // hand-edited freeze/receipt that stays internally self-consistent (its own
1172
+ // candidateDigest recomputed over its own, now-forged fields) must never be
1173
+ // trusted -- verifyFreezeManifest and advanceBaseline must re-derive every
1174
+ // bound field against live ground truth (authority, C15 ledger, package.json,
1175
+ // artifact containment), not just check the freeze's own internal consistency.
1176
+ // ---------------------------------------------------------------------------
1177
+
1178
+ test('REGRESSION round2 (critical): verifyFreezeManifest/advanceBaseline reject a hand-edited freeze that narrows remediatedFailureIds via a self-recomputed candidateDigest; F1 stays blocking and authority never rotates', () => {
1179
+ withTempRepo((root) => {
1180
+ const baseCommit = initRepo(root);
1181
+ writeLegacyBridge(root, { baseCommit });
1182
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1183
+ writeFile(path.join(root, 'packages/spec/scripts/round2-remediation-forged.mjs'), 'export const x = 1;\n');
1184
+ git(root, ['add', '-A']);
1185
+ git(root, ['commit', '-qm', 'boot window change']);
1186
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1187
+ const inst = cf.createChangeFirewall({ root });
1188
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1189
+ const authorityBefore = inst.loadBaselineAuthority();
1190
+
1191
+ const changedPaths = ['packages/spec/scripts/round2-remediation-forged.mjs'];
1192
+ inst.openFailure({
1193
+ failure_id: 'F1', paths: changedPaths, candidate_digest: cf.sha256Tag('cand'),
1194
+ evidence_digest: cf.sha256Tag('evi'), occurred_at: new Date().toISOString(),
1195
+ });
1196
+ inst.classifyFailure({
1197
+ failure_id: 'F1', primary_failure_class: 'framework_regression',
1198
+ adjudication_digest: cf.sha256Tag('adj'), occurred_at: new Date().toISOString(),
1199
+ });
1200
+ assert.equal(inst.deriveFailureState('F1').blocking, true);
1201
+
1202
+ const freeze = inst.createFreezeManifest({
1203
+ change_intent: 'benchmark_remediation', proposal_id: 'p', invariant_id: 'INV-1',
1204
+ hypothesis: 'h', owner_layer: 'benchmark_controller', primary_failure_class: 'framework_regression',
1205
+ paths: changedPaths, failure_ids: ['F1'], evidence: ['e'],
1206
+ true_positive_family: ['tp'], negative_control_family: ['nc'], rollback_condition: 'revert',
1207
+ });
1208
+ assert.deepEqual(freeze.remediatedFailureIds, ['F1']);
1209
+ inst.verifyFreezeManifest(); // sanity: valid immediately after creation
1210
+
1211
+ // Hand-edit the freeze: drop F1 from remediatedFailureIds, then recompute
1212
+ // candidateDigest over the tampered fields -- exactly the self-consistency
1213
+ // trick the round-1 exploit relied on to pass the old candidateDigest-only check.
1214
+ const freezePath = path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1215
+ const tampered = JSON.parse(fs.readFileSync(freezePath, 'utf8'));
1216
+ tampered.remediatedFailureIds = [];
1217
+ tampered.candidateDigest = cf.computeCandidateDigest({
1218
+ baselineAuthorityDigest: tampered.baselineAuthorityDigest, baselineGeneration: tampered.baselineGeneration,
1219
+ baselineCommit: tampered.baselineCommit, failureLedgerDigest: tampered.failureLedgerDigest,
1220
+ failureLedgerGeneration: tampered.failureLedgerGeneration, remediatedFailureIds: tampered.remediatedFailureIds,
1221
+ changedPaths: tampered.changedPaths, treeDigest: tampered.treeDigest,
1222
+ proposalDigest: tampered.proposalDigest, packageVersion: tampered.packageVersion,
1223
+ });
1224
+ fs.writeFileSync(freezePath, `${JSON.stringify(tampered, null, 2)}\n`);
1225
+
1226
+ assert.throws(() => inst.verifyFreezeManifest(), (error) => error.code === 'freeze_stale');
1227
+
1228
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-artifact-round2.tgz'), 'bytes');
1229
+ const artifactDigest = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/release-artifact-round2.tgz'));
1230
+ const receiptBytes = Buffer.from(`${JSON.stringify({
1231
+ schema_version: '1', receipt_id: 'r-round2', status: 'published',
1232
+ baselineAuthorityDigest: authorityBefore.digest, baselineGeneration: authorityBefore.record.generation,
1233
+ freezeDigest: cf.sha256Tag(fs.readFileSync(freezePath)), candidateDigest: tampered.candidateDigest,
1234
+ treeDigest: tampered.treeDigest, packageVersion: tampered.packageVersion,
1235
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/release-artifact-round2.tgz',
1236
+ artifactDigest, resolvedFailureIds: [],
1237
+ }, null, 2)}\n`);
1238
+ assert.throws(() => inst.advanceBaseline(receiptBytes), (error) => error.code === 'freeze_stale');
1239
+
1240
+ assert.equal(inst.loadBaselineAuthority().state, 'bootstrap', 'authority must never rotate off a rejected tampered freeze');
1241
+ assert.equal(inst.deriveFailureState('F1').blocking, true, 'F1 must still be blocking');
1242
+ });
1243
+ });
1244
+
1245
+ test('REGRESSION round2 (critical): verifyFreezeManifest rejects a freeze whose changedPaths smuggles an extra protected path not covered by its own remediatedFailureIds opened.paths union', () => {
1246
+ withTempRepo((root) => {
1247
+ const baseCommit = initRepo(root);
1248
+ writeLegacyBridge(root, { baseCommit });
1249
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1250
+ writeFile(path.join(root, 'packages/spec/scripts/round2-uncovered-f1.mjs'), 'export const x = 1;\n');
1251
+ git(root, ['add', '-A']);
1252
+ git(root, ['commit', '-qm', 'boot window change']);
1253
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1254
+ const inst = cf.createChangeFirewall({ root });
1255
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1256
+
1257
+ const pathF1 = 'packages/spec/scripts/round2-uncovered-f1.mjs';
1258
+ inst.openFailure({
1259
+ failure_id: 'F1', paths: [pathF1], candidate_digest: cf.sha256Tag('cand'),
1260
+ evidence_digest: cf.sha256Tag('evi'), occurred_at: new Date().toISOString(),
1261
+ });
1262
+ inst.classifyFailure({
1263
+ failure_id: 'F1', primary_failure_class: 'framework_regression',
1264
+ adjudication_digest: cf.sha256Tag('adj'), occurred_at: new Date().toISOString(),
1265
+ });
1266
+
1267
+ const freeze = inst.createFreezeManifest({
1268
+ change_intent: 'benchmark_remediation', proposal_id: 'p', invariant_id: 'INV-1',
1269
+ hypothesis: 'h', owner_layer: 'benchmark_controller', primary_failure_class: 'framework_regression',
1270
+ paths: [pathF1], failure_ids: ['F1'], evidence: ['e'],
1271
+ true_positive_family: ['tp'], negative_control_family: ['nc'], rollback_condition: 'revert',
1272
+ });
1273
+ assert.deepEqual(freeze.remediatedFailureIds, ['F1']);
1274
+ assert.deepEqual(freeze.changedPaths, [pathF1]);
1275
+ inst.verifyFreezeManifest(); // sanity: valid immediately after creation
1276
+
1277
+ // A second, unrelated protected-path change lands in the real working tree
1278
+ // -- never proposed, never covered by F1's own opened.paths.
1279
+ const pathExtra = 'packages/spec/scripts/round2-uncovered-extra.mjs';
1280
+ writeFile(path.join(root, pathExtra), 'export const y = 2;\n');
1281
+
1282
+ // Hand-edit the freeze to fold this extra real path into changedPaths
1283
+ // (matching the now-real diff) while leaving remediatedFailureIds
1284
+ // unchanged at ['F1'] -- then recompute treeDigest/candidateDigest so the
1285
+ // freeze stays internally self-consistent and re-derivation-against-reality
1286
+ // alone (changedPaths vs the real diff, treeDigest vs real content) cannot
1287
+ // distinguish it from a legitimate freeze.
1288
+ const freezePath = path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1289
+ const tampered = JSON.parse(fs.readFileSync(freezePath, 'utf8'));
1290
+ tampered.changedPaths = cf.sortedUnique([pathF1, pathExtra]);
1291
+ tampered.treeDigest = inst.computeProtectedTreeDigest(tampered.changedPaths);
1292
+ tampered.candidateDigest = cf.computeCandidateDigest({
1293
+ baselineAuthorityDigest: tampered.baselineAuthorityDigest, baselineGeneration: tampered.baselineGeneration,
1294
+ baselineCommit: tampered.baselineCommit, failureLedgerDigest: tampered.failureLedgerDigest,
1295
+ failureLedgerGeneration: tampered.failureLedgerGeneration, remediatedFailureIds: tampered.remediatedFailureIds,
1296
+ changedPaths: tampered.changedPaths, treeDigest: tampered.treeDigest,
1297
+ proposalDigest: tampered.proposalDigest, packageVersion: tampered.packageVersion,
1298
+ });
1299
+ fs.writeFileSync(freezePath, `${JSON.stringify(tampered, null, 2)}\n`);
1300
+
1301
+ // The plain re-derivation-against-reality checks alone would NOT catch this
1302
+ // (freeze.changedPaths now matches the real current diff exactly) -- only
1303
+ // the path-union invariant (pathsUnion === changedPaths) does.
1304
+ assert.deepEqual(inst.deriveProtectedChangedPaths(baseCommit), tampered.changedPaths);
1305
+ assert.throws(() => inst.verifyFreezeManifest(), (error) => error.code === 'freeze_stale');
1306
+ });
1307
+ });
1308
+
1309
+ test('REGRESSION round2 (critical): verifyFreezeManifest rejects a forged baselineCommit even when candidateDigest is recomputed to stay self-consistent', () => {
1310
+ withTempRepo((root) => {
1311
+ const baseCommit = initRepo(root);
1312
+ writeLegacyBridge(root, { baseCommit });
1313
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1314
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round2-forged-commit.cjs'), 'module.exports = {};\n');
1315
+ git(root, ['add', '-A']);
1316
+ git(root, ['commit', '-qm', 'boot window change']);
1317
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1318
+ const inst = cf.createChangeFirewall({ root });
1319
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1320
+
1321
+ const evo = buildEvolutionSpecFixture(root, { name: 'round2-forged-commit-spec' });
1322
+ const changedPaths = ['packages/spec/src/claude/scripts/round2-forged-commit.cjs'];
1323
+ inst.createFreezeManifest({
1324
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
1325
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
1326
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1327
+ });
1328
+ inst.verifyFreezeManifest(); // sanity: valid immediately after creation
1329
+
1330
+ const freezePath = path.join(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1331
+ const tampered = JSON.parse(fs.readFileSync(freezePath, 'utf8'));
1332
+ // A syntactically valid but forged commit id -- the fix must reject on the
1333
+ // authority-vs-freeze mismatch itself, regardless of whether it is a real commit.
1334
+ tampered.baselineCommit = '9'.repeat(40);
1335
+ tampered.candidateDigest = cf.computeCandidateDigest({
1336
+ baselineAuthorityDigest: tampered.baselineAuthorityDigest, baselineGeneration: tampered.baselineGeneration,
1337
+ baselineCommit: tampered.baselineCommit, failureLedgerDigest: tampered.failureLedgerDigest,
1338
+ failureLedgerGeneration: tampered.failureLedgerGeneration, remediatedFailureIds: tampered.remediatedFailureIds,
1339
+ changedPaths: tampered.changedPaths, treeDigest: tampered.treeDigest,
1340
+ proposalDigest: tampered.proposalDigest, packageVersion: tampered.packageVersion,
1341
+ });
1342
+ fs.writeFileSync(freezePath, `${JSON.stringify(tampered, null, 2)}\n`);
1343
+
1344
+ assert.throws(() => inst.verifyFreezeManifest(), (error) => error.code === 'freeze_stale');
1345
+ });
1346
+ });
1347
+
1348
+ test('REGRESSION round2 (critical): advanceBaseline independently re-verifies the current freeze before trusting it -- a freeze staled only by an intervening real C15 ledger event is rejected, never just candidateDigest-matched through', () => {
1349
+ withTempRepo((root) => {
1350
+ const baseCommit = initRepo(root);
1351
+ writeLegacyBridge(root, { baseCommit });
1352
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1353
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round2-stale-advance.cjs'), 'module.exports = {};\n');
1354
+ git(root, ['add', '-A']);
1355
+ git(root, ['commit', '-qm', 'boot window change']);
1356
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1357
+ const inst = cf.createChangeFirewall({ root });
1358
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1359
+ const authority = inst.loadBaselineAuthority();
1360
+
1361
+ const evo = buildEvolutionSpecFixture(root, { name: 'round2-stale-advance-spec' });
1362
+ const changedPaths = ['packages/spec/src/claude/scripts/round2-stale-advance.cjs'];
1363
+ const freeze = inst.createFreezeManifest({
1364
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
1365
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
1366
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1367
+ });
1368
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1369
+
1370
+ // Stale the freeze via a legitimate, untampered ledger event, then attempt
1371
+ // a direct advanceBaseline call against the now-stale-but-byte-identical freeze.
1372
+ inst.openFailure({
1373
+ failure_id: 'F-stale-advance', paths: ['packages/spec/scripts/round2-unrelated.mjs'], candidate_digest: cf.sha256Tag('c'),
1374
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
1375
+ });
1376
+
1377
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/release-artifact-round2-stale.tgz'), 'bytes');
1378
+ const artifactDigest = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/release-artifact-round2-stale.tgz'));
1379
+ const receiptBytes = Buffer.from(`${JSON.stringify({
1380
+ schema_version: '1', receipt_id: 'r-round2-stale', status: 'published',
1381
+ baselineAuthorityDigest: authority.digest, baselineGeneration: authority.record.generation,
1382
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest,
1383
+ treeDigest: freeze.treeDigest, packageVersion: freeze.packageVersion,
1384
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/release-artifact-round2-stale.tgz',
1385
+ artifactDigest, resolvedFailureIds: [],
1386
+ }, null, 2)}\n`);
1387
+ assert.throws(() => inst.advanceBaseline(receiptBytes), (error) => error.code === 'freeze_stale');
1388
+ assert.equal(inst.loadBaselineAuthority().state, 'bootstrap', 'authority must never rotate against a stale freeze');
1389
+ });
1390
+ });
1391
+
1392
+ test('REGRESSION round2 (high): advanceBaseline rejects an artifactPath redirected via a symlinked ancestor to another directory still inside the project root', () => {
1393
+ withTempRepo((root) => {
1394
+ const baseCommit = initRepo(root);
1395
+ writeLegacyBridge(root, { baseCommit });
1396
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1397
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round2-internal-symlink.cjs'), 'module.exports = {};\n');
1398
+ git(root, ['add', '-A']);
1399
+ git(root, ['commit', '-qm', 'boot window change']);
1400
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1401
+ const inst = cf.createChangeFirewall({ root });
1402
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1403
+
1404
+ const evo = buildEvolutionSpecFixture(root, { name: 'round2-internal-symlink-spec' });
1405
+ const changedPaths = ['packages/spec/src/claude/scripts/round2-internal-symlink.cjs'];
1406
+ const freeze = inst.createFreezeManifest({
1407
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
1408
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
1409
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1410
+ });
1411
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1412
+ const authority = inst.loadBaselineAuthority();
1413
+
1414
+ // A real directory elsewhere inside the project root (NOT under
1415
+ // .cafekit-release/), with a symlink placed *inside* .cafekit-release/
1416
+ // pointing at it. The final resolved target is a real file still inside
1417
+ // the project root, so a project-root-only containment check (round 1)
1418
+ // would wrongly accept this; containment must be checked against the real
1419
+ // .cafekit-release/ directory itself.
1420
+ const internalDir = path.join(root, 'packages/spec/round2-internal-dir');
1421
+ fs.mkdirSync(internalDir, { recursive: true });
1422
+ fs.writeFileSync(path.join(internalDir, 'real.tgz'), 'real-bytes');
1423
+ const realDigest = cf.sha256Tag(fs.readFileSync(path.join(internalDir, 'real.tgz')));
1424
+ fs.symlinkSync(internalDir, path.join(root, 'packages/spec/.cafekit-release/linked-internal-dir'), 'dir');
1425
+
1426
+ const receiptBytes = Buffer.from(`${JSON.stringify({
1427
+ schema_version: '1', receipt_id: 'r-round2-internal', status: 'published',
1428
+ baselineAuthorityDigest: authority.digest, baselineGeneration: authority.record.generation,
1429
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest,
1430
+ treeDigest: freeze.treeDigest, packageVersion: freeze.packageVersion,
1431
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/linked-internal-dir/real.tgz',
1432
+ artifactDigest: realDigest, resolvedFailureIds: [],
1433
+ }, null, 2)}\n`);
1434
+ assert.throws(() => inst.advanceBaseline(receiptBytes), (error) => error.code === 'artifact_invalid');
1435
+ assert.equal(inst.loadBaselineAuthority().state, 'bootstrap', 'authority must never rotate on a rejected artifact path');
1436
+ });
1437
+ });
1438
+
1439
+ test('REGRESSION round2 (critical): advanceBaseline rejects an artifactPath when packages/spec/.cafekit-release/ itself is a symlink, even to a directory still inside the project root', () => {
1440
+ withTempRepo((root) => {
1441
+ // Replace the real .cafekit-release/ (created by initRepo) with a symlink
1442
+ // to another real directory still inside the project root, before doing
1443
+ // anything else. A relative-containment-only check would never catch
1444
+ // this: both the artifact candidate and the release directory itself
1445
+ // resolve through the identical redirection, so they always appear
1446
+ // "contained" relative to each other no matter where it points.
1447
+ const baseCommit = initRepo(root);
1448
+ fs.rmSync(path.join(root, 'packages/spec/.cafekit-release'), { recursive: true, force: true });
1449
+ const actualReleaseDir = path.join(root, 'packages/spec/actual-cafekit-release');
1450
+ fs.mkdirSync(actualReleaseDir, { recursive: true });
1451
+ fs.symlinkSync(actualReleaseDir, path.join(root, 'packages/spec/.cafekit-release'), 'dir');
1452
+
1453
+ writeLegacyBridge(root, { baseCommit });
1454
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1455
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round2-release-dir-symlink.cjs'), 'module.exports = {};\n');
1456
+ git(root, ['add', '-A']);
1457
+ git(root, ['commit', '-qm', 'boot window change']);
1458
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1459
+ const inst = cf.createChangeFirewall({ root });
1460
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1461
+
1462
+ const evo = buildEvolutionSpecFixture(root, { name: 'round2-release-dir-symlink-spec' });
1463
+ const changedPaths = ['packages/spec/src/claude/scripts/round2-release-dir-symlink.cjs'];
1464
+ const freeze = inst.createFreezeManifest({
1465
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
1466
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
1467
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1468
+ });
1469
+ // Writes through the symlinked .cafekit-release/ land transparently in
1470
+ // actualReleaseDir; freeze creation/read logic never inspects it for a
1471
+ // symlinked ancestor, so this proceeds exactly like a normal checkout.
1472
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1473
+ const authority = inst.loadBaselineAuthority();
1474
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/artifact.tgz'), 'bytes');
1475
+ const artifactDigest = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/artifact.tgz'));
1476
+
1477
+ const receiptBytes = Buffer.from(`${JSON.stringify({
1478
+ schema_version: '1', receipt_id: 'r-round2-releasedir-symlink', status: 'published',
1479
+ baselineAuthorityDigest: authority.digest, baselineGeneration: authority.record.generation,
1480
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest,
1481
+ treeDigest: freeze.treeDigest, packageVersion: freeze.packageVersion,
1482
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/artifact.tgz',
1483
+ artifactDigest, resolvedFailureIds: [],
1484
+ }, null, 2)}\n`);
1485
+ assert.throws(() => inst.advanceBaseline(receiptBytes), (error) => error.code === 'artifact_invalid');
1486
+ assert.equal(inst.loadBaselineAuthority().state, 'bootstrap', 'authority must never rotate when .cafekit-release/ itself is a symlink');
1487
+ });
1488
+ });
1489
+
1490
+ test('REGRESSION round2 (critical): advanceBaseline rejects an artifactPath redirected by a symlink planted inside .cafekit-release/ to another real subdir still inside the release tree', () => {
1491
+ withTempRepo((root) => {
1492
+ const baseCommit = initRepo(root);
1493
+ writeLegacyBridge(root, { baseCommit });
1494
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1495
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round2-internal-release-symlink.cjs'), 'module.exports = {};\n');
1496
+ git(root, ['add', '-A']);
1497
+ git(root, ['commit', '-qm', 'boot window change']);
1498
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1499
+ const inst = cf.createChangeFirewall({ root });
1500
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1501
+
1502
+ const evo = buildEvolutionSpecFixture(root, { name: 'round2-internal-release-symlink-spec' });
1503
+ const changedPaths = ['packages/spec/src/claude/scripts/round2-internal-release-symlink.cjs'];
1504
+ const freeze = inst.createFreezeManifest({
1505
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
1506
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
1507
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1508
+ });
1509
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1510
+ const authority = inst.loadBaselineAuthority();
1511
+
1512
+ // A real subdirectory *of the real .cafekit-release/ itself*, plus a
1513
+ // symlink also planted inside .cafekit-release/ that redirects to it --
1514
+ // the resolved target never leaves the real release tree at all, so a
1515
+ // containment-relative-to-release-dir check alone would wrongly accept it.
1516
+ const realSubdir = path.join(root, 'packages/spec/.cafekit-release/real-target-subdir');
1517
+ fs.mkdirSync(realSubdir, { recursive: true });
1518
+ fs.writeFileSync(path.join(realSubdir, 'real.tgz'), 'real-bytes');
1519
+ const realDigest = cf.sha256Tag(fs.readFileSync(path.join(realSubdir, 'real.tgz')));
1520
+ fs.symlinkSync(realSubdir, path.join(root, 'packages/spec/.cafekit-release/redirect-subdir'), 'dir');
1521
+
1522
+ const receiptBytes = Buffer.from(`${JSON.stringify({
1523
+ schema_version: '1', receipt_id: 'r-round2-internal-release', status: 'published',
1524
+ baselineAuthorityDigest: authority.digest, baselineGeneration: authority.record.generation,
1525
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest,
1526
+ treeDigest: freeze.treeDigest, packageVersion: freeze.packageVersion,
1527
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/redirect-subdir/real.tgz',
1528
+ artifactDigest: realDigest, resolvedFailureIds: [],
1529
+ }, null, 2)}\n`);
1530
+ assert.throws(() => inst.advanceBaseline(receiptBytes), (error) => error.code === 'artifact_invalid');
1531
+ assert.equal(inst.loadBaselineAuthority().state, 'bootstrap', 'authority must never rotate on an internal release-dir symlink redirect');
1532
+ });
1533
+ });
1534
+
1535
+ // ---------------------------------------------------------------------------
1536
+ // Shared committed-history provenance invariant (round4): every committed
1537
+ // snapshot pair is validated -- not merely the last-commit-vs-current hop --
1538
+ // for both C10 authority and C15 ledger, with an explicit fail-closed guard
1539
+ // against an undetectable or shallow git history.
1540
+ // ---------------------------------------------------------------------------
1541
+
1542
+ test('REGRESSION round4 (critical): a committed released C10 record hand-edited in place (same generation) fails closed even after an unrelated later commit', () => {
1543
+ withTempRepo((root) => {
1544
+ const baseCommit = initRepo(root);
1545
+ writeLegacyBridge(root, { baseCommit });
1546
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1547
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round4-released-tamper.cjs'), 'module.exports = {};\n');
1548
+ git(root, ['add', '-A']);
1549
+ git(root, ['commit', '-qm', 'boot window change']);
1550
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1551
+ const inst = cf.createChangeFirewall({ root });
1552
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1553
+
1554
+ const evo = buildEvolutionSpecFixture(root, { name: 'round4-released-spec' });
1555
+ const changedPaths = ['packages/spec/src/claude/scripts/round4-released-tamper.cjs'];
1556
+ const freeze = inst.createFreezeManifest({
1557
+ change_intent: 'authorized_evolution', proposal_id: 'p', spec_ref: evo.specRef,
1558
+ spec_semantic_digest: evo.digest, independent_pass_receipt_digest: evo.reviewReceiptDigest,
1559
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1560
+ });
1561
+ const authority = inst.loadBaselineAuthority();
1562
+ const freezeBytes = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1563
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/round4-artifact.tgz'), 'artifact-bytes');
1564
+ const artifactDigest = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/round4-artifact.tgz'));
1565
+ const receiptBytes = Buffer.from(`${JSON.stringify({
1566
+ schema_version: '1', receipt_id: 'r-round4', status: 'published',
1567
+ baselineAuthorityDigest: authority.digest, baselineGeneration: authority.record.generation,
1568
+ freezeDigest: cf.sha256Tag(freezeBytes), candidateDigest: freeze.candidateDigest,
1569
+ treeDigest: freeze.treeDigest, packageVersion: freeze.packageVersion,
1570
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/round4-artifact.tgz',
1571
+ artifactDigest, resolvedFailureIds: [],
1572
+ }, null, 2)}\n`);
1573
+ inst.advanceBaseline(receiptBytes);
1574
+ assert.equal(inst.loadBaselineAuthority().state, 'released');
1575
+
1576
+ // Commit the genuine released record (a real, legitimate commit).
1577
+ git(root, ['add', '-A']);
1578
+ git(root, ['commit', '-qm', 'released record committed']);
1579
+
1580
+ // Same-generation in-place tamper, itself committed as an ordinary commit.
1581
+ const authorityPath = path.join(root, 'packages/spec/benchmarks/release-baseline.json');
1582
+ const genuine = JSON.parse(fs.readFileSync(authorityPath, 'utf8'));
1583
+ const tampered = { ...genuine, baselinePackageVersion: `${genuine.baselinePackageVersion}-tampered` };
1584
+ fs.writeFileSync(authorityPath, `${JSON.stringify(tampered, null, 2)}\n`);
1585
+ git(root, ['add', '-A']);
1586
+ git(root, ['commit', '-qm', 'same-generation released tamper']);
1587
+
1588
+ // An unrelated later commit, touching a different file entirely.
1589
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round4-unrelated.cjs'), 'module.exports = {};\n');
1590
+ git(root, ['add', '-A']);
1591
+ git(root, ['commit', '-qm', 'unrelated later commit']);
1592
+
1593
+ // Before this fix, provenance compared only the last commit against
1594
+ // current -- and this unrelated commit doesn't even touch
1595
+ // release-baseline.json, so "the last committed version of this file" is
1596
+ // still the tampered one, byte-identical to current, and was trusted
1597
+ // forever. The shared committed-history walk now finds the
1598
+ // genuine->tampered pair among release-baseline.json's own committed
1599
+ // snapshots and rejects it regardless of what committed afterward.
1600
+ assert.throws(() => inst.assertBaselineUsable(), (error) => error.code === 'out_of_band_edit');
1601
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'out_of_band_edit');
1602
+ });
1603
+ });
1604
+
1605
+ test('REGRESSION round4 (critical): a committed C15 ledger record hand-edited in place (same generation) fails closed even after an unrelated later commit', () => {
1606
+ withTempRepo((root) => {
1607
+ initRepo(root);
1608
+ const inst = cf.createChangeFirewall({ root });
1609
+ inst.openFailure({
1610
+ failure_id: 'F-round4', paths: ['packages/spec/scripts/round4-ledger.mjs'], candidate_digest: cf.sha256Tag('c'),
1611
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
1612
+ });
1613
+ git(root, ['add', '-A']);
1614
+ git(root, ['commit', '-qm', 'ledger opened F-round4']);
1615
+
1616
+ // Same-generation in-place event tamper, itself committed.
1617
+ const ledgerPath = path.join(root, 'packages/spec/benchmarks/benchmark-failure-ledger.json');
1618
+ const tampered = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
1619
+ tampered.events[0].paths = ['packages/spec/scripts/round4-ledger-tampered.mjs'];
1620
+ fs.writeFileSync(ledgerPath, `${JSON.stringify(tampered, null, 2)}\n`);
1621
+ git(root, ['add', '-A']);
1622
+ git(root, ['commit', '-qm', 'same-generation ledger tamper']);
1623
+
1624
+ // An unrelated later commit, touching a different file entirely.
1625
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round4-ledger-unrelated.cjs'), 'module.exports = {};\n');
1626
+ git(root, ['add', '-A']);
1627
+ git(root, ['commit', '-qm', 'unrelated later commit']);
1628
+
1629
+ assert.throws(() => inst.assertFailureLedgerUsable(), (error) => error.code === 'out_of_band_edit');
1630
+ assert.throws(
1631
+ () => inst.classifyFailure({
1632
+ failure_id: 'F-round4', primary_failure_class: 'model_runtime_error',
1633
+ adjudication_digest: cf.sha256Tag('adj'), occurred_at: new Date().toISOString(),
1634
+ }),
1635
+ (error) => error.code === 'out_of_band_edit',
1636
+ );
1637
+ });
1638
+ });
1639
+
1640
+ // ---------------------------------------------------------------------------
1641
+ // Review5 (round5): the canonical contract (design.md C10, "an out-of-band
1642
+ // edit": generation must equal priorGeneration + 1) admits no generation-jump
1643
+ // exemption once a committed predecessor exists -- not even for several named
1644
+ // transitions squashed uncommitted into one later commit. The prior "C15
1645
+ // squash compatibility" positive test above asserted exactly the behavior
1646
+ // this contract forbids and is replaced by the contract-accurate regressions
1647
+ // below.
1648
+ // ---------------------------------------------------------------------------
1649
+
1650
+ test('REGRESSION round5 (critical): a C10 committed generation jump with a schema-valid record and fabricated previousDigest rejects', () => {
1651
+ withTempRepo((root) => {
1652
+ const baseCommit = initRepo(root);
1653
+ writeJson(path.join(root, 'packages/spec/benchmarks/release-baseline.json'), {
1654
+ state: 'bootstrap', generation: 0, baselineCommit: baseCommit,
1655
+ baselinePackageVersion: '0.0.1-fixture', bootstrapReviewDigest: cf.sha256Tag('seed'),
1656
+ previousBaselineDigest: null,
1657
+ });
1658
+ git(root, ['add', '-A']);
1659
+ git(root, ['commit', '-qm', 'genesis bootstrap committed']);
1660
+
1661
+ // A schema-valid released record at generation 2 -- skipping generation 1
1662
+ // entirely -- with a well-formed but fabricated previousBaselineDigest.
1663
+ // Shape validation alone would accept this; only the exact
1664
+ // generation-must-advance-by-one rule catches it.
1665
+ writeJson(path.join(root, 'packages/spec/benchmarks/release-baseline.json'), {
1666
+ state: 'released', generation: 2, baselineCommit: baseCommit,
1667
+ baselinePackageVersion: '0.0.1-fixture', releaseReceiptDigest: cf.sha256Tag('receipt'),
1668
+ previousBaselineDigest: cf.sha256Tag('fabricated-predecessor'),
1669
+ });
1670
+ git(root, ['add', '-A']);
1671
+ git(root, ['commit', '-qm', 'fabricated generation-2 released record']);
1672
+
1673
+ const inst = cf.createChangeFirewall({ root });
1674
+ assert.throws(() => inst.assertBaselineUsable(), (error) => error.code === 'out_of_band_edit');
1675
+ });
1676
+ });
1677
+
1678
+ test('REGRESSION round5 (high): a C15 committed generation jump with a fully shape-valid event count and fabricated previousDigest rejects', () => {
1679
+ withTempRepo((root) => {
1680
+ initRepo(root);
1681
+ const inst = cf.createChangeFirewall({ root });
1682
+ inst.openFailure({
1683
+ failure_id: 'F-round5', paths: ['packages/spec/scripts/round5-ledger.mjs'], candidate_digest: cf.sha256Tag('c'),
1684
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
1685
+ });
1686
+ git(root, ['add', '-A']);
1687
+ git(root, ['commit', '-qm', 'genesis ledger committed']);
1688
+
1689
+ // A schema-valid ledger record at generation 2 -- events.length matches
1690
+ // generation + 1 exactly and every event independently validates -- but
1691
+ // it skips generation 1 entirely and previousLedgerDigest is fabricated.
1692
+ // Shape validation alone would accept this; only the exact generation
1693
+ // rule catches it.
1694
+ const ledgerPath = path.join(root, 'packages/spec/benchmarks/benchmark-failure-ledger.json');
1695
+ const genuine = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
1696
+ writeJson(ledgerPath, {
1697
+ schema_version: '1', generation: 2, previousLedgerDigest: cf.sha256Tag('fabricated-predecessor'),
1698
+ events: [
1699
+ genuine.events[0],
1700
+ {
1701
+ type: 'classified', failure_id: 'F-round5', primary_failure_class: 'model_runtime_error',
1702
+ adjudication_digest: cf.sha256Tag('adj-1'), occurred_at: new Date().toISOString(),
1703
+ },
1704
+ {
1705
+ type: 'reclassified', failure_id: 'F-round5', previous_primary_failure_class: 'model_runtime_error',
1706
+ primary_failure_class: 'framework_regression', adjudication_digest: cf.sha256Tag('adj-2'), occurred_at: new Date().toISOString(),
1707
+ },
1708
+ ],
1709
+ });
1710
+ git(root, ['add', '-A']);
1711
+ git(root, ['commit', '-qm', 'fabricated generation-2 ledger record']);
1712
+
1713
+ assert.throws(() => inst.assertFailureLedgerUsable(), (error) => error.code === 'out_of_band_edit');
1714
+ });
1715
+ });
1716
+
1717
+ test('REGRESSION round5 (critical): a second uncommitted C15 named transition relative to the same committed predecessor is rejected before write, leaving file bytes unchanged', () => {
1718
+ withTempRepo((root) => {
1719
+ initRepo(root);
1720
+ const inst = cf.createChangeFirewall({ root });
1721
+ inst.openFailure({
1722
+ failure_id: 'F-round5b', paths: ['packages/spec/scripts/round5b-ledger.mjs'], candidate_digest: cf.sha256Tag('c'),
1723
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
1724
+ });
1725
+ git(root, ['add', '-A']);
1726
+ git(root, ['commit', '-qm', 'genesis ledger committed']); // generation 0, committed predecessor
1727
+
1728
+ // First uncommitted transition: exactly one step beyond the committed
1729
+ // predecessor -- accepted.
1730
+ inst.classifyFailure({
1731
+ failure_id: 'F-round5b', primary_failure_class: 'model_runtime_error',
1732
+ adjudication_digest: cf.sha256Tag('adj-1'), occurred_at: new Date().toISOString(),
1733
+ });
1734
+ const ledgerPath = path.join(root, 'packages/spec/benchmarks/benchmark-failure-ledger.json');
1735
+ const afterFirstTransition = fs.readFileSync(ledgerPath);
1736
+
1737
+ // Second uncommitted transition, still relative to the same committed
1738
+ // (generation 0) predecessor -- would jump the committed chain by two.
1739
+ // Rejected before ever touching the file: bytes stay exactly as the
1740
+ // first transition left them, not a corrupted generation-2 state that
1741
+ // only the next load would have caught.
1742
+ assert.throws(
1743
+ () => inst.reclassifyFailure({
1744
+ failure_id: 'F-round5b', previous_primary_failure_class: 'model_runtime_error', primary_failure_class: 'framework_regression',
1745
+ adjudication_digest: cf.sha256Tag('adj-2'), occurred_at: new Date().toISOString(),
1746
+ }),
1747
+ (error) => error.code === 'out_of_band_edit',
1748
+ );
1749
+ assert.deepEqual(fs.readFileSync(ledgerPath), afterFirstTransition, 'file bytes must remain exactly as the first valid transition left them');
1750
+ });
1751
+ });
1752
+
1753
+ test('a normal exact-plus-one uncommitted C15 named transition after a committed predecessor remains accepted', () => {
1754
+ withTempRepo((root) => {
1755
+ initRepo(root);
1756
+ const inst = cf.createChangeFirewall({ root });
1757
+ inst.openFailure({
1758
+ failure_id: 'F-round5c', paths: ['packages/spec/scripts/round5c-ledger.mjs'], candidate_digest: cf.sha256Tag('c'),
1759
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
1760
+ });
1761
+ git(root, ['add', '-A']);
1762
+ git(root, ['commit', '-qm', 'genesis ledger committed']);
1763
+
1764
+ const record = inst.classifyFailure({
1765
+ failure_id: 'F-round5c', primary_failure_class: 'model_runtime_error',
1766
+ adjudication_digest: cf.sha256Tag('adj-1'), occurred_at: new Date().toISOString(),
1767
+ });
1768
+ assert.equal(record.generation, 1);
1769
+ assert.equal(inst.assertFailureLedgerUsable().record.generation, 1);
1770
+ });
1771
+ });
1772
+
1773
+ test('multiple uncommitted C15 named transitions remain accepted when there is no committed predecessor at all', () => {
1774
+ withTempRepo((root) => {
1775
+ initRepo(root);
1776
+ const inst = cf.createChangeFirewall({ root });
1777
+ inst.openFailure({
1778
+ failure_id: 'F-round5e', paths: ['packages/spec/scripts/round5e-ledger.mjs'], candidate_digest: cf.sha256Tag('c'),
1779
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
1780
+ });
1781
+ inst.classifyFailure({
1782
+ failure_id: 'F-round5e', primary_failure_class: 'model_runtime_error',
1783
+ adjudication_digest: cf.sha256Tag('adj-1'), occurred_at: new Date().toISOString(),
1784
+ });
1785
+ const record = inst.reclassifyFailure({
1786
+ failure_id: 'F-round5e', previous_primary_failure_class: 'model_runtime_error', primary_failure_class: 'framework_regression',
1787
+ adjudication_digest: cf.sha256Tag('adj-2'), occurred_at: new Date().toISOString(),
1788
+ });
1789
+ // None of this has ever been committed -- case (2) of C10's own load
1790
+ // rules trusts it as first-ever state, however many uncommitted
1791
+ // transitions produced it.
1792
+ assert.equal(record.generation, 2);
1793
+ assert.equal(inst.assertFailureLedgerUsable().record.generation, 2);
1794
+ });
1795
+ });
1796
+
1797
+ test('REGRESSION round4 (critical): a shallow/ambiguous committed history fails closed instead of trusting the fetched HEAD as genesis', () => {
1798
+ withTempRepo((srcRoot) => {
1799
+ const baseCommit = initRepo(srcRoot);
1800
+ writeLegacyBridge(srcRoot, { baseCommit });
1801
+ const reviewReceiptDigest = writeFeatureSeedPass(srcRoot);
1802
+ writeFile(path.join(srcRoot, 'packages/spec/src/claude/scripts/round4-shallow.cjs'), 'module.exports = {};\n');
1803
+ git(srcRoot, ['add', '-A']);
1804
+ git(srcRoot, ['commit', '-qm', 'boot window change']);
1805
+ const headCommit = git(srcRoot, ['rev-parse', 'HEAD']).trim();
1806
+ const srcInst = cf.createChangeFirewall({ root: srcRoot });
1807
+ srcInst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1808
+ git(srcRoot, ['add', '-A']);
1809
+ git(srcRoot, ['commit', '-qm', 'bootstrap record committed']);
1810
+
1811
+ const shallowParent = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-shallow-'));
1812
+ try {
1813
+ git(shallowParent, ['clone', '--no-local', '--depth', '1', srcRoot, 'clone']);
1814
+ const shallowRoot = path.join(shallowParent, 'clone');
1815
+ assert.equal(
1816
+ git(shallowRoot, ['rev-parse', '--is-shallow-repository']).trim(), 'true',
1817
+ 'fixture sanity: the clone must actually be shallow',
1818
+ );
1819
+ const shallowInst = cf.createChangeFirewall({ root: shallowRoot });
1820
+
1821
+ // The shallow clone's HEAD carries a perfectly well-formed bootstrap
1822
+ // record -- the truncated fetched history simply cannot prove it is the
1823
+ // *true* genesis. Before this fix, a git-log/show ambiguity over an
1824
+ // incomplete history was silently treated as "nothing committed,
1825
+ // trusted"; now it must fail closed rather than accept the fetched HEAD
1826
+ // as a false, unprovable trust root.
1827
+ assert.throws(() => shallowInst.assertBaselineUsable(), (error) => error.code === 'out_of_band_edit');
1828
+ } finally {
1829
+ fs.rmSync(shallowParent, { recursive: true, force: true });
1830
+ }
1831
+ });
1832
+ });
1833
+
1834
+ // ---------------------------------------------------------------------------
1835
+ // Review6 (round6): readCommittedSnapshots must fail closed whenever
1836
+ // committed history exists for a reserved path but HEAD currently has no
1837
+ // blob for it (a committed deletion no named transition ever performs), not
1838
+ // silently treat "currently absent" as "never committed"; and
1839
+ // gitChangedPathsRaw must never allowlist git change statuses by an
1840
+ // enumerated diff-filter that can silently stop covering a real status (T
1841
+ // type-change, U unmerged) it never anticipated.
1842
+ // ---------------------------------------------------------------------------
1843
+
1844
+ test('REGRESSION round6 (critical): a committed C10 authority git-rm-committed then recreated uncommitted is rejected, not trusted as a fresh first record', () => {
1845
+ withTempRepo((root) => {
1846
+ const baseCommit = initRepo(root);
1847
+ writeLegacyBridge(root, { baseCommit });
1848
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1849
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round6-c10-delete.cjs'), 'module.exports = {};\n');
1850
+ git(root, ['add', '-A']);
1851
+ git(root, ['commit', '-qm', 'boot window change']);
1852
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1853
+ const inst = cf.createChangeFirewall({ root });
1854
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1855
+
1856
+ const authorityPath = path.join(root, 'packages/spec/benchmarks/release-baseline.json');
1857
+ git(root, ['add', '-A']);
1858
+ git(root, ['commit', '-qm', 'genuine bootstrap record committed']);
1859
+
1860
+ // git-rm the committed authority and commit the deletion -- no named
1861
+ // transition ever deletes this file, so committed history now shows a
1862
+ // real deletion, not "this path never existed."
1863
+ git(root, ['rm', '-q', 'packages/spec/benchmarks/release-baseline.json']);
1864
+ git(root, ['commit', '-qm', 'authority deleted']);
1865
+ assert.equal(fs.existsSync(authorityPath), false);
1866
+
1867
+ // Recreate a brand-new-looking bootstrap record directly, uncommitted --
1868
+ // before this fix, gitHeadFileBytes returning null for the (now-deleted)
1869
+ // path short-circuited readCommittedSnapshots straight to "never
1870
+ // committed, trusted" without ever consulting git log.
1871
+ writeJson(authorityPath, {
1872
+ state: 'bootstrap', generation: 0, baselineCommit: baseCommit,
1873
+ baselinePackageVersion: '0.0.1-fixture', bootstrapReviewDigest: reviewReceiptDigest,
1874
+ previousBaselineDigest: null,
1875
+ });
1876
+ assert.throws(() => inst.assertBaselineUsable(), (error) => error.code === 'out_of_band_edit');
1877
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'out_of_band_edit');
1878
+
1879
+ // Even bootstrapBaseline's own writer-side candidate check refuses to
1880
+ // launder a fresh record into the deleted path's place.
1881
+ fs.rmSync(authorityPath);
1882
+ const headCommit2 = git(root, ['rev-parse', 'HEAD']).trim();
1883
+ assert.throws(
1884
+ () => inst.bootstrapBaseline({ baseCommit, headCommit: headCommit2, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' }),
1885
+ (error) => error.code === 'out_of_band_edit',
1886
+ );
1887
+ });
1888
+ });
1889
+
1890
+ test('REGRESSION round6 (critical): a committed C15 ledger git-rm-committed then recreated uncommitted is rejected, not trusted as a fresh first record', () => {
1891
+ withTempRepo((root) => {
1892
+ initRepo(root);
1893
+ const inst = cf.createChangeFirewall({ root });
1894
+ inst.openFailure({
1895
+ failure_id: 'F-round6', paths: ['packages/spec/scripts/round6-ledger.mjs'], candidate_digest: cf.sha256Tag('c'),
1896
+ evidence_digest: cf.sha256Tag('e'), occurred_at: new Date().toISOString(),
1897
+ });
1898
+ const ledgerPath = path.join(root, 'packages/spec/benchmarks/benchmark-failure-ledger.json');
1899
+ git(root, ['add', '-A']);
1900
+ git(root, ['commit', '-qm', 'genuine ledger record committed']);
1901
+
1902
+ git(root, ['rm', '-q', 'packages/spec/benchmarks/benchmark-failure-ledger.json']);
1903
+ git(root, ['commit', '-qm', 'ledger deleted']);
1904
+ assert.equal(fs.existsSync(ledgerPath), false);
1905
+
1906
+ // Recreate a brand-new-looking generation-0 ledger directly, uncommitted.
1907
+ writeJson(ledgerPath, {
1908
+ schema_version: '1', generation: 0, previousLedgerDigest: null,
1909
+ events: [{
1910
+ type: 'opened', failure_id: 'F-round6-fresh', paths: ['packages/spec/scripts/round6-ledger-fresh.mjs'],
1911
+ candidate_digest: cf.sha256Tag('c2'), evidence_digest: cf.sha256Tag('e2'), occurred_at: new Date().toISOString(),
1912
+ }],
1913
+ });
1914
+ assert.throws(() => inst.assertFailureLedgerUsable(), (error) => error.code === 'out_of_band_edit');
1915
+
1916
+ // Even openFailure's own writer-side candidate check refuses to launder a
1917
+ // fresh record into the deleted path's place.
1918
+ fs.rmSync(ledgerPath);
1919
+ assert.throws(
1920
+ () => inst.openFailure({
1921
+ failure_id: 'F-round6-second', paths: ['packages/spec/scripts/round6-ledger-second.mjs'], candidate_digest: cf.sha256Tag('c3'),
1922
+ evidence_digest: cf.sha256Tag('e3'), occurred_at: new Date().toISOString(),
1923
+ }),
1924
+ (error) => error.code === 'out_of_band_edit',
1925
+ );
1926
+ });
1927
+ });
1928
+
1929
+ test('REGRESSION round6 (high): a protected regular file swapped to a symlink (type-change) enters D1 derivation and is gated without reading outside the root', () => {
1930
+ withTempRepo((root) => {
1931
+ initRepo(root);
1932
+ const protectedRelPath = 'packages/spec/scripts/round6-typechange.mjs';
1933
+ writeFile(path.join(root, protectedRelPath), 'export const x = 1;\n');
1934
+ git(root, ['add', '-A']);
1935
+ git(root, ['commit', '-qm', 'protected file committed before the boot window']);
1936
+ const baseCommit = git(root, ['rev-parse', 'HEAD']).trim();
1937
+ writeLegacyBridge(root, { baseCommit });
1938
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1939
+
1940
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round6-typechange-boot.cjs'), 'module.exports = {};\n');
1941
+ git(root, ['add', '-A']);
1942
+ git(root, ['commit', '-qm', 'boot window change']);
1943
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1944
+
1945
+ const inst = cf.createChangeFirewall({ root });
1946
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1947
+
1948
+ // Type-change: the already-committed protected regular file (present
1949
+ // since baseCommit, unrelated to the boot-window diff) is swapped for a
1950
+ // symlink pointing at a target outside the project root that never even
1951
+ // needs to exist -- the digest/derivation must never dereference it.
1952
+ const protectedAbsPath = path.join(root, protectedRelPath);
1953
+ fs.unlinkSync(protectedAbsPath);
1954
+ fs.symlinkSync('/nonexistent-outside-target-round6', protectedAbsPath);
1955
+
1956
+ // Prior to the diff-filter fix, --diff-filter=ACDMR silently dropped this
1957
+ // T (type-changed) status path from derivation entirely.
1958
+ const changedPaths = inst.deriveProtectedChangedPaths(baseCommit);
1959
+ assert.ok(changedPaths.includes(protectedRelPath), 'a type-changed protected path must enter D1 derivation, not be silently dropped');
1960
+
1961
+ // The gate correctly demands a proposal for this now-visible protected
1962
+ // change; computing that gate/tree digest must not read the (nonexistent)
1963
+ // external symlink target.
1964
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'proposal_required');
1965
+ const treeDigest = inst.computeProtectedTreeDigest(changedPaths);
1966
+ assert.match(treeDigest, /^sha256:[0-9a-f]{64}$/);
1967
+ });
1968
+ });
1969
+
1970
+ test('REGRESSION round6 (critical): a second uncommitted C10 advanceBaseline call relative to the same committed predecessor is rejected before mutation, leaving bytes unchanged', () => {
1971
+ withTempRepo((root) => {
1972
+ const baseCommit = initRepo(root);
1973
+ writeLegacyBridge(root, { baseCommit });
1974
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
1975
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round6-c10-second-advance.cjs'), 'module.exports = {};\n');
1976
+ git(root, ['add', '-A']);
1977
+ git(root, ['commit', '-qm', 'boot window change']);
1978
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
1979
+ const inst = cf.createChangeFirewall({ root });
1980
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
1981
+
1982
+ // Commit the genuine bootstrap record: this is the committed predecessor.
1983
+ git(root, ['add', '-A']);
1984
+ git(root, ['commit', '-qm', 'bootstrap record committed']);
1985
+
1986
+ // First advanceBaseline call: released generation 1, exactly one step
1987
+ // beyond the committed bootstrap predecessor -- accepted, left uncommitted.
1988
+ const evo1 = buildEvolutionSpecFixture(root, { name: 'round6-second-advance-spec-1' });
1989
+ const changedPaths = ['packages/spec/src/claude/scripts/round6-c10-second-advance.cjs'];
1990
+ const freeze1 = inst.createFreezeManifest({
1991
+ change_intent: 'authorized_evolution', proposal_id: 'p1', spec_ref: evo1.specRef,
1992
+ spec_semantic_digest: evo1.digest, independent_pass_receipt_digest: evo1.reviewReceiptDigest,
1993
+ planned_write_set: changedPaths, rollback_condition: 'r', negative_control_family: ['nc'],
1994
+ });
1995
+ const authority0 = inst.loadBaselineAuthority();
1996
+ const freezeBytes1 = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
1997
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/round6-artifact-1.tgz'), 'artifact-bytes-1');
1998
+ const artifactDigest1 = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/round6-artifact-1.tgz'));
1999
+ const receiptBytes1 = Buffer.from(`${JSON.stringify({
2000
+ schema_version: '1', receipt_id: 'r-round6-1', status: 'published',
2001
+ baselineAuthorityDigest: authority0.digest, baselineGeneration: authority0.record.generation,
2002
+ freezeDigest: cf.sha256Tag(freezeBytes1), candidateDigest: freeze1.candidateDigest,
2003
+ treeDigest: freeze1.treeDigest, packageVersion: freeze1.packageVersion,
2004
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/round6-artifact-1.tgz',
2005
+ artifactDigest: artifactDigest1, resolvedFailureIds: [],
2006
+ }, null, 2)}\n`);
2007
+ inst.advanceBaseline(receiptBytes1);
2008
+ const authorityPath = path.join(root, 'packages/spec/benchmarks/release-baseline.json');
2009
+ const receiptPath = path.join(root, 'packages/spec/.cafekit-release/release-receipt.json');
2010
+ const afterFirstAdvance = fs.readFileSync(authorityPath);
2011
+ const receiptAfterFirstAdvance = fs.readFileSync(receiptPath);
2012
+ assert.equal(inst.loadBaselineAuthority().record.generation, 1);
2013
+
2014
+ // Second advanceBaseline call, against a freshly-built no_protected_change
2015
+ // freeze/receipt for the current (still-uncommitted) generation-1
2016
+ // authority -- would jump the *committed* chain (still only generation 0)
2017
+ // by two. Rejected before ever touching either release-baseline.json or
2018
+ // release-receipt.json's bytes: prepare-then-validate-all builds and
2019
+ // verifies Event (B)'s candidate against committed provenance *before*
2020
+ // Event (A) (the receipt) is ever written, so a rejection here leaves
2021
+ // both files exactly as the first, successful advance left them -- not
2022
+ // a corrupted receipt paired with an untouched authority record.
2023
+ const freeze2 = inst.createFreezeManifest(null);
2024
+ const authority1 = inst.loadBaselineAuthority();
2025
+ const freezeBytes2 = readFileBytes(root, 'packages/spec/.cafekit-release/change-firewall-freeze.json');
2026
+ fs.writeFileSync(path.join(root, 'packages/spec/.cafekit-release/round6-artifact-2.tgz'), 'artifact-bytes-2');
2027
+ const artifactDigest2 = cf.sha256Tag(readFileBytes(root, 'packages/spec/.cafekit-release/round6-artifact-2.tgz'));
2028
+ const receiptBytes2 = Buffer.from(`${JSON.stringify({
2029
+ schema_version: '1', receipt_id: 'r-round6-2', status: 'published',
2030
+ baselineAuthorityDigest: authority1.digest, baselineGeneration: authority1.record.generation,
2031
+ freezeDigest: cf.sha256Tag(freezeBytes2), candidateDigest: freeze2.candidateDigest,
2032
+ treeDigest: freeze2.treeDigest, packageVersion: freeze2.packageVersion,
2033
+ releaseCommit: headCommit, artifactPath: 'packages/spec/.cafekit-release/round6-artifact-2.tgz',
2034
+ artifactDigest: artifactDigest2, resolvedFailureIds: [],
2035
+ }, null, 2)}\n`);
2036
+ assert.throws(() => inst.advanceBaseline(receiptBytes2), (error) => error.code === 'out_of_band_edit');
2037
+ assert.deepEqual(fs.readFileSync(authorityPath), afterFirstAdvance, 'release-baseline.json bytes must remain exactly as the first advance left them');
2038
+ assert.deepEqual(fs.readFileSync(receiptPath), receiptAfterFirstAdvance, 'release-receipt.json bytes must remain exactly as the first advance left them -- Event (A) must never run before Event (B)\'s candidate is validated');
2039
+ });
2040
+ });
2041
+
2042
+ // ---------------------------------------------------------------------------
2043
+ // Review7 (round7): advanceBaseline's prepare/validate-all-before-first-write
2044
+ // invariant (proven above); a genuine git status U (unmerged) protected path
2045
+ // must enter D1 derivation and be gated exactly like any other change; a
2046
+ // reserved control-plane path renamed away and back (or recreated at the same
2047
+ // path) must never be trusted as first-ever provenance.
2048
+ // ---------------------------------------------------------------------------
2049
+
2050
+ test('REGRESSION round7 (high): a genuinely unmerged (git status U) protected path still enters D1 derivation and is gated without reading outside the root', () => {
2051
+ withTempRepo((root) => {
2052
+ const baseCommit = initRepo(root);
2053
+ writeLegacyBridge(root, { baseCommit });
2054
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
2055
+ const protectedRelPath = 'packages/spec/scripts/round7-unmerged.mjs';
2056
+ writeFile(path.join(root, protectedRelPath), 'export const x = 0;\n');
2057
+ git(root, ['add', '-A']);
2058
+ git(root, ['commit', '-qm', 'boot window change']);
2059
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
2060
+ const mainBranch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
2061
+
2062
+ const inst = cf.createChangeFirewall({ root });
2063
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
2064
+ git(root, ['add', '-A']);
2065
+ git(root, ['commit', '-qm', 'bootstrap record committed']);
2066
+
2067
+ git(root, ['checkout', '-qb', 'round7-branch-a']);
2068
+ writeFile(path.join(root, protectedRelPath), 'export const x = 1;\n');
2069
+ git(root, ['add', '-A']);
2070
+ git(root, ['commit', '-qm', 'branch a change']);
2071
+
2072
+ git(root, ['checkout', '-q', mainBranch]);
2073
+ writeFile(path.join(root, protectedRelPath), 'export const x = 2;\n');
2074
+ git(root, ['add', '-A']);
2075
+ git(root, ['commit', '-qm', 'branch b change']);
2076
+
2077
+ // A genuine merge attempt -- both branches touched the same line of the
2078
+ // same protected file, so this must conflict, leaving real unmerged
2079
+ // (multi-stage index) entries and conflict markers on disk, not a
2080
+ // simulated/hand-crafted stand-in.
2081
+ const mergeResult = spawnSync('git', ['-C', root, 'merge', '--no-commit', '--no-ff', 'round7-branch-a'], { encoding: 'utf8' });
2082
+ assert.notEqual(mergeResult.status, 0, 'fixture sanity: the merge must actually conflict');
2083
+ assert.match(git(root, ['status', '--porcelain']), /^UU /m, 'fixture sanity: the protected path must show as a genuine unmerged (UU) status');
2084
+
2085
+ const changedPaths = inst.deriveProtectedChangedPaths(baseCommit);
2086
+ assert.ok(changedPaths.includes(protectedRelPath), 'an unmerged protected path must enter D1 derivation, not be silently dropped');
2087
+
2088
+ // Computing the gate/tree digest over this unmerged path must not crash
2089
+ // or read anything outside the trusted root -- readWorktreeEntry reads
2090
+ // the on-disk (conflict-marked) file bytes directly, the same as any
2091
+ // other regular file, never dereferencing anything external.
2092
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'proposal_required');
2093
+ const treeDigest = inst.computeProtectedTreeDigest(changedPaths);
2094
+ assert.match(treeDigest, /^sha256:[0-9a-f]{64}$/);
2095
+ });
2096
+ });
2097
+
2098
+ test('REGRESSION round7 (critical): a committed C10 authority renamed away then renamed back to the reserved path is never trusted as first-ever provenance', () => {
2099
+ withTempRepo((root) => {
2100
+ const baseCommit = initRepo(root);
2101
+ writeLegacyBridge(root, { baseCommit });
2102
+ const reviewReceiptDigest = writeFeatureSeedPass(root);
2103
+ writeFile(path.join(root, 'packages/spec/src/claude/scripts/round7-rename.cjs'), 'module.exports = {};\n');
2104
+ git(root, ['add', '-A']);
2105
+ git(root, ['commit', '-qm', 'boot window change']);
2106
+ const headCommit = git(root, ['rev-parse', 'HEAD']).trim();
2107
+ const inst = cf.createChangeFirewall({ root });
2108
+ inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: reviewReceiptDigest, packageVersion: '0.0.1-fixture' });
2109
+
2110
+ const authorityPath = path.join(root, 'packages/spec/benchmarks/release-baseline.json');
2111
+ git(root, ['add', '-A']);
2112
+ git(root, ['commit', '-qm', 'genuine bootstrap record committed']);
2113
+
2114
+ // Rename the reserved path away, committed -- git log -- <reserved path>
2115
+ // still shows this commit as a real touch (removal) of that exact path,
2116
+ // exactly like a plain deletion.
2117
+ git(root, ['mv', 'packages/spec/benchmarks/release-baseline.json', 'packages/spec/benchmarks/round7-renamed-away.json']);
2118
+ git(root, ['commit', '-qm', 'authority renamed away']);
2119
+ assert.equal(fs.existsSync(authorityPath), false);
2120
+
2121
+ // Rename it back to the exact reserved path, committed -- the bytes are
2122
+ // byte-identical to the original (a pure rename, no content change), yet
2123
+ // the path's own committed history still shows a real absence in
2124
+ // between: no named transition ever deletes or renames this file, so
2125
+ // this must never be laundered back in as a trusted, unbroken chain.
2126
+ git(root, ['mv', 'packages/spec/benchmarks/round7-renamed-away.json', 'packages/spec/benchmarks/release-baseline.json']);
2127
+ git(root, ['commit', '-qm', 'authority renamed back']);
2128
+ assert.equal(fs.existsSync(authorityPath), true);
2129
+
2130
+ assert.throws(() => inst.assertBaselineUsable(), (error) => error.code === 'out_of_band_edit');
2131
+ assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'out_of_band_edit');
2132
+ });
2133
+ });
2134
+