@haposoft/cafekit 0.16.0-rc.4 → 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 (28) hide show
  1. package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
  2. package/bin/__tests__/codex-hooks.test.js +136 -64
  3. package/bin/__tests__/develop-contract.test.js +55 -17
  4. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
  5. package/bin/__tests__/package-inventory.test.js +29 -11
  6. package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
  7. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
  8. package/bin/__tests__/specs-v2-execution-closeout.test.js +59 -12
  9. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +78 -14
  10. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +222 -42
  11. package/bin/__tests__/validator-grounding.test.js +103 -4
  12. package/package.json +5 -2
  13. package/src/claude/hooks/__tests__/completion-authority.test.js +31 -1
  14. package/src/claude/hooks/__tests__/privacy-block.test.js +111 -29
  15. package/src/claude/hooks/privacy-block.cjs +38 -14
  16. package/src/claude/scripts/change-firewall.cjs +1748 -0
  17. package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
  18. package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
  19. package/src/claude/scripts/spec-final-state.cjs +46 -0
  20. package/src/claude/scripts/spec-ground.cjs +46 -10
  21. package/src/claude/scripts/spec-readiness.cjs +324 -17
  22. package/src/claude/scripts/spec-scaffold.cjs +268 -28
  23. package/src/claude/scripts/validate-spec-output.cjs +262 -10
  24. package/src/claude/skills/specs/references/review.md +14 -2
  25. package/src/claude/skills/specs/templates/spec-state.json +14 -2
  26. package/src/codex/hooks/lib/privacy-state.cjs +23 -22
  27. package/src/codex/hooks/privacy-approval.cjs +7 -3
  28. package/src/codex/hooks/privacy-block.cjs +45 -22
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const { finalizeReadiness } = require('../../src/claude/scripts/spec-readiness.cjs');
10
+
11
+ const SEMANTIC_DIGEST = `sha256:${'a'.repeat(64)}`;
12
+ const FEATURE_NAME = 'cafekit-semantic-eval-firewall';
13
+ const CREATED_AT = '2026-08-17T00:00:00.000Z';
14
+ const AUTHORING_VALIDATION = Object.freeze({
15
+ schema_version: '1',
16
+ requirements: { digest: `sha256:${'b'.repeat(64)}`, validated_at: CREATED_AT },
17
+ design: { digest: `sha256:${'c'.repeat(64)}`, validated_at: CREATED_AT },
18
+ research: { digest: `sha256:${'d'.repeat(64)}`, validated_at: CREATED_AT },
19
+ tasks: { digest: `sha256:${'e'.repeat(64)}`, validated_at: CREATED_AT },
20
+ });
21
+
22
+ function review(verdict = 'PASS', overrides = {}) {
23
+ return {
24
+ verdict,
25
+ findings: [],
26
+ unresolved_decisions: [],
27
+ graph_coverage: [{
28
+ surface: 'criterion_local',
29
+ covered: true,
30
+ notes: 'criterion was reviewed against the canonical semantic model',
31
+ }],
32
+ reviewed_criteria: ['R1.1'],
33
+ counterexamples: [],
34
+ reviewer_evidence: null,
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ function finding(id, blocking = false) {
40
+ return {
41
+ id,
42
+ severity: 'High',
43
+ location: 'spec.json:R1.1',
44
+ summary: 'The semantic review requires an explicit disposition',
45
+ blocking,
46
+ disposition: 'open',
47
+ };
48
+ }
49
+
50
+ function decision(id, blocking = false) {
51
+ return {
52
+ id,
53
+ summary: 'The unresolved decision needs an owner and a recorded outcome',
54
+ blocking,
55
+ };
56
+ }
57
+
58
+ function stableJson(value) {
59
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
60
+ if (value && typeof value === 'object') {
61
+ return `{${Object.keys(value).sort().map((key) => (
62
+ `${JSON.stringify(key)}:${stableJson(value[key])}`
63
+ )).join(',')}}`;
64
+ }
65
+ return JSON.stringify(value);
66
+ }
67
+
68
+ function dependencies({ staleAuthoring = false } = {}) {
69
+ const calls = { digest: 0, validate: 0, ground: 0 };
70
+ const expectedAuthoring = JSON.stringify(AUTHORING_VALIDATION);
71
+ return {
72
+ calls,
73
+ semantic: {
74
+ modelFromMarkdown: () => ({ model: { projected: true }, errors: [] }),
75
+ stableJson,
76
+ },
77
+ validator: {
78
+ computeSemanticDigest21: () => {
79
+ calls.digest += 1;
80
+ return { errors: [], digest: SEMANTIC_DIGEST };
81
+ },
82
+ validateSpec: (_directory, candidate) => {
83
+ calls.validate += 1;
84
+ const preserved = JSON.stringify(candidate.validation.authoring_validation) === expectedAuthoring;
85
+ return { errors: staleAuthoring || !preserved ? ['authoring validation is stale'] : [], warnings: [] };
86
+ },
87
+ },
88
+ grounder: () => {
89
+ calls.ground += 1;
90
+ return { errors: [] };
91
+ },
92
+ };
93
+ }
94
+
95
+ function fixture({ identity = { feature_name: FEATURE_NAME, created_at: CREATED_AT },
96
+ semanticReviewHistory, authoringValidation = AUTHORING_VALIDATION } = {}) {
97
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-readiness-'));
98
+ const validation = {
99
+ status: 'completed',
100
+ semantic_review: null,
101
+ authoring_validation: authoringValidation,
102
+ };
103
+ if (semanticReviewHistory !== undefined) validation.semantic_review_history = semanticReviewHistory;
104
+ const spec = {
105
+ schema_version: '2.1',
106
+ ...identity,
107
+ semantic_model: { projected: false },
108
+ validation,
109
+ ready_for_implementation: false,
110
+ timestamps: {
111
+ init: CREATED_AT,
112
+ requirements_done: CREATED_AT,
113
+ design_done: CREATED_AT,
114
+ validation_done: CREATED_AT,
115
+ },
116
+ updated_at: CREATED_AT,
117
+ };
118
+ fs.writeFileSync(path.join(directory, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
119
+ return { directory, spec };
120
+ }
121
+
122
+ function finalize(directory, reviewResult, options = {}) {
123
+ const injected = dependencies(options);
124
+ const result = finalizeReadiness({
125
+ specDir: directory,
126
+ projectRoot: directory,
127
+ reviewResult,
128
+ ...injected,
129
+ });
130
+ return { result, calls: injected.calls };
131
+ }
132
+
133
+ function cleanup(directory) {
134
+ fs.rmSync(directory, { recursive: true, force: true });
135
+ }
136
+
137
+ test('explicit PASS builds a completed C2 receipt, preserves C16, and is ready', () => {
138
+ const { directory } = fixture();
139
+ try {
140
+ const { result, calls } = finalize(directory, review('PASS'));
141
+ assert.equal(result.spec.ready_for_implementation, true);
142
+ assert.equal(result.spec.validation.semantic_review.status, 'completed');
143
+ assert.equal(result.spec.validation.semantic_review.verdict, 'PASS');
144
+ assert.deepEqual(result.spec.validation.semantic_review.findings, []);
145
+ assert.deepEqual(result.spec.validation.authoring_validation, AUTHORING_VALIDATION);
146
+ assert.match(result.history_entry.review_receipt_digest, /^sha256:[0-9a-f]{64}$/);
147
+ assert.equal(calls.validate, 2);
148
+ assert.equal(calls.ground, 2);
149
+ } finally {
150
+ cleanup(directory);
151
+ }
152
+ });
153
+
154
+ test('explicit FAIL is authoritative even when there are no blockers', () => {
155
+ const { directory } = fixture();
156
+ try {
157
+ const { result } = finalize(directory, review('FAIL'));
158
+ assert.equal(result.spec.ready_for_implementation, false);
159
+ assert.equal(result.history_entry.verdict, 'FAIL');
160
+ assert.equal(result.history_entry.blocking_count, 0);
161
+ assert.equal(result.history_entry.lifecycle_disposition, 'CONTINUE');
162
+ } finally {
163
+ cleanup(directory);
164
+ }
165
+ });
166
+
167
+ test('PASS with blocking findings or decisions is refused', () => {
168
+ for (const overrides of [
169
+ { findings: [finding('F-BLOCKING', true)] },
170
+ { unresolved_decisions: [decision('D-BLOCKING', true)] },
171
+ ]) {
172
+ const { directory } = fixture();
173
+ try {
174
+ assert.throws(
175
+ () => finalize(directory, review('PASS', overrides)),
176
+ /PASS cannot contain blocking/,
177
+ );
178
+ const persisted = JSON.parse(fs.readFileSync(path.join(directory, 'spec.json'), 'utf8'));
179
+ assert.equal(persisted.validation.semantic_review_history, undefined);
180
+ } finally {
181
+ cleanup(directory);
182
+ }
183
+ }
184
+ });
185
+
186
+ test('blocking_count counts blocking findings and unresolved decisions', () => {
187
+ const { directory } = fixture();
188
+ try {
189
+ const { result } = finalize(directory, review('FAIL', {
190
+ findings: [finding('F-BLOCKING', true), finding('F-OPEN', false)],
191
+ unresolved_decisions: [decision('D-BLOCKING', true), decision('D-OPEN', false)],
192
+ }));
193
+ assert.equal(result.history_entry.blocking_count, 2);
194
+ } finally {
195
+ cleanup(directory);
196
+ }
197
+ });
198
+
199
+ test('caller digest and derived/control fields are rejected from public input', () => {
200
+ const fields = [
201
+ 'review_receipt_digest', 'lifecycle_disposition', 'repair_round', 'review_epoch',
202
+ 'attempt_index', 'semantic_review_history', 'lineage', 'readiness',
203
+ 'ready_for_implementation', 'validation', 'authoring_validation',
204
+ ];
205
+ for (const field of fields) {
206
+ const { directory } = fixture();
207
+ try {
208
+ const invalid = { ...review(), [field]: field };
209
+ assert.throws(() => finalize(directory, invalid), /fields must be exactly/);
210
+ } finally {
211
+ cleanup(directory);
212
+ }
213
+ }
214
+ });
215
+
216
+ test('lineage is deterministic and divergent or missing identity is rejected', () => {
217
+ const firstFixture = fixture();
218
+ const secondFixture = fixture();
219
+ const divergentFixture = fixture({
220
+ semanticReviewHistory: { lineage_id: `sha256:${'f'.repeat(64)}`, entries: [] },
221
+ });
222
+ const missingFixture = fixture({ identity: { created_at: CREATED_AT } });
223
+ try {
224
+ const first = finalize(firstFixture.directory, review()).result;
225
+ const second = finalize(secondFixture.directory, review()).result;
226
+ assert.equal(
227
+ first.spec.validation.semantic_review_history.lineage_id,
228
+ second.spec.validation.semantic_review_history.lineage_id,
229
+ );
230
+ assert.match(first.spec.validation.semantic_review_history.lineage_id, /^sha256:[0-9a-f]{64}$/);
231
+ assert.throws(
232
+ () => finalize(divergentFixture.directory, review()),
233
+ /lineage_id diverges/,
234
+ );
235
+ assert.throws(
236
+ () => finalize(missingFixture.directory, review()),
237
+ /feature_name is required/,
238
+ );
239
+ } finally {
240
+ cleanup(firstFixture.directory);
241
+ cleanup(secondFixture.directory);
242
+ cleanup(divergentFixture.directory);
243
+ cleanup(missingFixture.directory);
244
+ }
245
+ });
246
+
247
+ test('replaying a receipt is idempotent across the whole lineage', () => {
248
+ const { directory } = fixture();
249
+ try {
250
+ finalize(directory, review('PASS'));
251
+ const before = fs.readFileSync(path.join(directory, 'spec.json'), 'utf8');
252
+ const replay = finalize(directory, review('PASS')).result;
253
+ const after = fs.readFileSync(path.join(directory, 'spec.json'), 'utf8');
254
+ assert.equal(replay.replayed, true);
255
+ assert.equal(replay.spec.validation.semantic_review_history.entries.length, 1);
256
+ assert.equal(after, before);
257
+ } finally {
258
+ cleanup(directory);
259
+ }
260
+ });
261
+
262
+ test('replaying an earlier receipt dedupes across the whole lineage', () => {
263
+ const { directory } = fixture();
264
+ try {
265
+ finalize(directory, review('PASS', { reviewed_criteria: ['R1.1'] }));
266
+ finalize(directory, review('FAIL', { reviewed_criteria: ['R1.2'] }));
267
+ const specFile = path.join(directory, 'spec.json');
268
+ const beforeReplay = fs.readFileSync(specFile);
269
+ const replay = finalize(directory, review('PASS', { reviewed_criteria: ['R1.1'] })).result;
270
+ const afterReplay = fs.readFileSync(specFile);
271
+ assert.equal(replay.replayed, true);
272
+ assert.equal(replay.history_entry.sequence, 0);
273
+ assert.equal(replay.history_entry.review_epoch, 0);
274
+ assert.equal(replay.history_entry.attempt_index, 0);
275
+ assert.equal(replay.spec.validation.semantic_review_history.entries.length, 2);
276
+ assert.deepEqual(afterReplay, beforeReplay);
277
+ } finally {
278
+ cleanup(directory);
279
+ }
280
+ });
281
+
282
+ test('a PASS starts the next epoch with a reset FAIL attempt index', () => {
283
+ const { directory } = fixture();
284
+ try {
285
+ finalize(directory, review('PASS'));
286
+ finalize(directory, review('FAIL', { reviewed_criteria: ['R1.2'] }));
287
+ finalize(directory, review('PASS', { reviewed_criteria: ['R1.3'] }));
288
+ const next = finalize(directory, review('FAIL', { reviewed_criteria: ['R1.4'] })).result;
289
+ assert.equal(next.history_entry.review_epoch, 2);
290
+ assert.equal(next.history_entry.attempt_index, 0);
291
+ assert.equal(next.history_entry.lifecycle_disposition, 'CONTINUE');
292
+ } finally {
293
+ cleanup(directory);
294
+ }
295
+ });
296
+
297
+ test('the third FAIL is terminal BLOCKED and there is no index 3', () => {
298
+ const { directory } = fixture();
299
+ try {
300
+ finalize(directory, review('FAIL', { reviewed_criteria: ['R1.1'] }));
301
+ finalize(directory, review('FAIL', { reviewed_criteria: ['R1.2'] }));
302
+ const blocked = finalize(directory, review('FAIL', { reviewed_criteria: ['R1.3'] })).result;
303
+ assert.equal(blocked.spec.ready_for_implementation, false);
304
+ assert.equal(blocked.history_entry.attempt_index, 2);
305
+ assert.equal(blocked.history_entry.lifecycle_disposition, 'BLOCKED');
306
+ const specFile = path.join(directory, 'spec.json');
307
+ const beforeReplay = fs.readFileSync(specFile);
308
+ const replay = finalize(directory, review('FAIL', { reviewed_criteria: ['R1.3'] })).result;
309
+ const afterReplay = fs.readFileSync(specFile);
310
+ assert.equal(replay.replayed, true);
311
+ assert.equal(replay.history_entry.attempt_index, 2);
312
+ assert.deepEqual(afterReplay, beforeReplay);
313
+ assert.throws(() => finalize(directory, review('PASS')), /already ends BLOCKED/);
314
+ const persisted = JSON.parse(afterReplay.toString('utf8'));
315
+ assert.deepEqual(
316
+ persisted.validation.semantic_review_history.entries.map((entry) => entry.attempt_index),
317
+ [0, 1, 2],
318
+ );
319
+ } finally {
320
+ cleanup(directory);
321
+ }
322
+ });
323
+
324
+ test('stale C16 authoring validation refuses the append', () => {
325
+ const { directory } = fixture({
326
+ authoringValidation: { ...AUTHORING_VALIDATION, design: { digest: 'stale' } },
327
+ });
328
+ try {
329
+ const specFile = path.join(directory, 'spec.json');
330
+ const before = fs.readFileSync(specFile);
331
+ assert.throws(
332
+ () => finalize(directory, review('PASS'), { staleAuthoring: true }),
333
+ /readiness validation failed/,
334
+ );
335
+ const after = fs.readFileSync(specFile);
336
+ assert.deepEqual(after, before);
337
+ const persisted = JSON.parse(after.toString('utf8'));
338
+ assert.equal(persisted.validation.semantic_review_history, undefined);
339
+ } finally {
340
+ cleanup(directory);
341
+ }
342
+ });
@@ -20,6 +20,8 @@ const REQUIRED_PAYLOAD = [
20
20
  'src/claude/scripts/spec-readiness.cjs',
21
21
  'src/claude/scripts/spec-semantic-model.cjs',
22
22
  'src/claude/scripts/validate-spec-output.cjs',
23
+ 'src/claude/scripts/spec-authoring-validation.cjs',
24
+ 'src/claude/scripts/spec-authoring-digest.cjs',
23
25
  ];
24
26
  const FORBIDDEN_PAYLOAD = [
25
27
  /(^|\/)\.logs(\/|$)/,
@@ -413,7 +415,10 @@ function installedSemanticPaths(root, platform) {
413
415
  : path.join(runtimeRoot, 'skills');
414
416
  const paths = {
415
417
  policy: path.join(runtimeRoot, 'scripts', 'workflow-policy.cjs'),
418
+ scaffold: path.join(runtimeRoot, 'scripts', 'spec-scaffold.cjs'),
416
419
  validator: path.join(runtimeRoot, 'scripts', 'validate-spec-output.cjs'),
420
+ authoringValidator: path.join(runtimeRoot, 'scripts', 'spec-authoring-validation.cjs'),
421
+ authoringDigest: path.join(runtimeRoot, 'scripts', 'spec-authoring-digest.cjs'),
417
422
  grounder: path.join(runtimeRoot, 'scripts', 'spec-ground.cjs'),
418
423
  finalState: path.join(runtimeRoot, 'scripts', 'spec-final-state.cjs'),
419
424
  readiness: path.join(runtimeRoot, 'scripts', 'spec-readiness.cjs'),
@@ -443,12 +448,8 @@ function writeJson(target, value) {
443
448
 
444
449
  function materializeSpecState(paths, root, feature, { strict = false } = {}) {
445
450
  const featureDir = path.join(root, 'specs', feature);
446
- fs.mkdirSync(featureDir, { recursive: true });
447
- const source = fs.readFileSync(paths.stateTemplate, 'utf8')
448
- .replaceAll('{{FEATURE_NAME}}', feature)
449
- .replaceAll('{{TIMESTAMP}}', '2026-08-14T00:00:00.000Z')
450
- .replaceAll('{{PROJECT_DESCRIPTION}}', `${feature} installed semantic fixture`);
451
- const state = JSON.parse(source);
451
+ runInstalled(paths.scaffold, [feature], root);
452
+ const state = JSON.parse(fs.readFileSync(path.join(featureDir, 'spec.json'), 'utf8'));
452
453
  state.scope_lock.in_scope = [`${feature} behavior`];
453
454
  state.scope_lock.out_of_scope = ['unrelated behavior'];
454
455
  if (strict) {
@@ -535,12 +536,29 @@ Any input other than the boolean \`true\` never returns enabled.
535
536
  }
536
537
 
537
538
  function completeSemanticReview(paths, root, fixture, counterexamples) {
539
+ // C16/D13: only the installed spec-authoring-validation.cjs coordinator may
540
+ // flip authoring.* to validated and write the matching receipt (I21/R3.9) —
541
+ // run it over the fixture's current bytes instead of hand-writing the enum.
542
+ // Lifecycle order (I15/R3.7): authoring -> coordinator -> semantic review ->
543
+ // readiness, never the reverse.
544
+ runInstalled(paths.scaffold, [fixture.state.feature_name, '--sync-semantic-model'], root);
545
+ runInstalled(paths.authoringValidator, [fixture.featureDir], root);
538
546
  const state = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
539
- state.authoring.requirements = 'validated';
540
- state.authoring.design = 'validated';
541
- if (state.authoring.tasks !== 'absent') state.authoring.tasks = 'validated';
542
- writeJson(fixture.specFile, state);
543
- fixture.reviewResult = { reviewed_criteria: counterexamples.map(({ criterion }) => criterion), counterexamples };
547
+ fixture.reviewResult = {
548
+ verdict: 'PASS',
549
+ findings: [],
550
+ unresolved_decisions: [],
551
+ graph_coverage: [
552
+ 'criterion_local', 'cross_criterion', 'runtime_path',
553
+ 'assumption_provenance', 'compatibility_migration',
554
+ ].map((surface) => ({
555
+ surface, covered: true,
556
+ notes: 'The review covers this semantic surface against the canonical model.',
557
+ })),
558
+ reviewed_criteria: counterexamples.map(({ criterion }) => criterion),
559
+ counterexamples,
560
+ reviewer_evidence: null,
561
+ };
544
562
  if (state.workflow_policy.assurance_level === 'Strict') {
545
563
  const digest = runInstalled(paths.validator, [fixture.featureDir, '--semantic-digest'], root).stdout.trim();
546
564
  assert.match(digest, /^sha256:[a-f0-9]{64}$/);