@haposoft/cafekit 0.16.0-rc.4 → 0.16.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +136 -64
- package/bin/__tests__/develop-contract.test.js +55 -17
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/package-inventory.test.js +29 -11
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +586 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +59 -12
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +78 -14
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +222 -42
- package/bin/__tests__/validator-grounding.test.js +103 -4
- package/package.json +3 -2
- package/src/claude/hooks/__tests__/completion-authority.test.js +31 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +111 -29
- package/src/claude/hooks/privacy-block.cjs +38 -14
- package/src/claude/scripts/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +46 -0
- package/src/claude/scripts/spec-ground.cjs +46 -10
- package/src/claude/scripts/spec-readiness.cjs +324 -17
- package/src/claude/scripts/spec-scaffold.cjs +268 -28
- package/src/claude/scripts/validate-spec-output.cjs +262 -10
- package/src/claude/skills/specs/references/review.md +14 -2
- package/src/claude/skills/specs/templates/spec-state.json +14 -2
- package/src/codex/hooks/lib/privacy-state.cjs +23 -22
- package/src/codex/hooks/privacy-approval.cjs +7 -3
- package/src/codex/hooks/privacy-block.cjs +45 -22
|
@@ -0,0 +1,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
|
-
|
|
447
|
-
const
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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}$/);
|