@haposoft/cafekit 0.16.0-rc.6 → 0.16.0-rc.8
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/README.md +21 -58
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
- package/bin/__tests__/codex-hooks.test.js +382 -0
- package/bin/__tests__/codex-native.test.js +579 -80
- package/bin/__tests__/develop-contract.test.js +155 -76
- package/bin/__tests__/installer-safety.test.js +19 -154
- package/bin/__tests__/package-inventory.test.js +494 -26
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
- package/bin/__tests__/validator-grounding.test.js +11 -11
- package/bin/install.js +2 -7
- package/bin/lib/context.js +0 -45
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +0 -3
- package/bin/lib/manifest.js +1 -1
- package/bin/phases/claude-runtime.js +1 -3
- package/bin/phases/codex-runtime.js +4 -2
- package/bin/phases/copy-payload.js +3 -15
- package/bin/phases/post-install.js +5 -17
- package/bin/phases/root-config.js +2 -3
- package/bin/phases/select-platform.js +2 -5
- package/bin/phases/summary.js +0 -1
- package/package.json +2 -5
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +82 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/lib/context.cjs +1 -1
- package/src/claude/hooks/lib/detect.cjs +1 -1
- package/src/claude/hooks/spec-gate.cjs +44 -9
- package/src/claude/hooks/spec-state.cjs +51 -13
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/migration-manifest.json +17 -3
- package/src/claude/rules/manage-docs.md +43 -24
- package/src/claude/rules/state-sync.md +57 -62
- package/src/claude/rules/workflow.md +30 -13
- package/src/claude/scripts/change-firewall.cjs +2 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
- package/src/claude/scripts/spec-receipt.cjs +196 -1
- package/src/claude/scripts/spec-resolver.cjs +428 -0
- package/src/claude/skills/develop/SKILL.md +89 -125
- package/src/claude/skills/develop/references/parallel-waves.md +75 -162
- package/src/claude/skills/develop/references/quality-gate.md +61 -121
- package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
- package/src/claude/skills/question/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +152 -204
- package/src/claude/skills/specs/references/review.md +124 -243
- package/src/claude/skills/specs/references/templates.md +202 -0
- package/src/claude/skills/sync/SKILL.md +66 -90
- package/src/claude/skills/sync/references/sync-protocols.md +62 -83
- package/src/codex/AGENTS.md +20 -7
- package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
- package/src/codex/hooks/lib/spec-utils.cjs +25 -0
- package/src/codex/hooks/spec-gate.cjs +46 -16
- package/src/codex/hooks/spec-state.cjs +54 -16
- package/src/codex/rules/state-sync.md +57 -61
- package/src/common/AGENTS.md +8 -1
- package/bin/__tests__/opencode-spec-gate.test.js +0 -799
- package/bin/lib/opencode-install.js +0 -899
- package/bin/phases/opencode-runtime.js +0 -38
- package/src/claude/skills/specs/references/archive-workflow.md +0 -74
- package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
- package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
- package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
- package/src/claude/skills/specs/references/research-strategy.md +0 -107
- package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
- package/src/claude/skills/specs/references/translation-mirror.md +0 -83
- package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
- package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
- package/src/claude/skills/specs/rules/design-principles.md +0 -138
- package/src/claude/skills/specs/rules/design-review.md +0 -55
- package/src/claude/skills/specs/rules/ears-format.md +0 -42
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
- package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
- package/src/opencode/AGENTS.md +0 -41
- package/src/opencode/plugins/docs-sync.ts +0 -175
- package/src/opencode/plugins/inspect-block.ts +0 -164
- package/src/opencode/plugins/privacy-block.ts +0 -182
- package/src/opencode/plugins/rules.ts +0 -155
- package/src/opencode/plugins/session.ts +0 -179
- package/src/opencode/plugins/spec-gate.ts +0 -457
- package/src/opencode/plugins/spec-state.ts +0 -311
- package/src/opencode/plugins/state.ts +0 -253
- package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
- package/src/opencode/plugins/usage.ts +0 -178
|
@@ -4,10 +4,47 @@ const assert = require('node:assert/strict');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
+
const { createHash } = require('node:crypto');
|
|
7
8
|
const { spawnSync } = require('node:child_process');
|
|
8
9
|
const { test } = require('node:test');
|
|
9
10
|
|
|
10
11
|
const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
12
|
+
const PACKAGE_VERSION = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
13
|
+
const OLD_FIXTURE_VERSION = '0.14.1';
|
|
14
|
+
const NO_INVENTION_ANCHOR = 'Before implementation handoff, apply the **no-invention gate**:';
|
|
15
|
+
const IMPLEMENTATION_READINESS_BOUNDARY_ROWS = [
|
|
16
|
+
['API/CLI', 'entrypoint/route or command grammar; identity/auth; input/default/normalization; success output; error/status/exit; duplicate/retry/idempotency; compatibility'],
|
|
17
|
+
['Schema', 'version; exact keys/nesting/types; required/optional; enum/format/bounds/cardinality; unknown-field behavior; compatibility/migration'],
|
|
18
|
+
['State/concurrency', 'initial/terminal states; event + guard + effect + next + error; ordering; duplicate/retry; writer/lock acquire/contention/release; rollback/recovery'],
|
|
19
|
+
['Filesystem/security', 'authoritative root; trusted/untrusted segment grammar; lexical + canonical containment and symlink policy; flags/mode; temp/rename/fsync; lock/stale reclaim; crash cleanup'],
|
|
20
|
+
['Time/retention', 'clock source; unit/precision/timezone; endpoints and inclusion/comparator; anomaly behavior; expiry/purge/recovery'],
|
|
21
|
+
['Integration/proof', 'caller; export/registration; packaging/config; native path/consumer; proof level (`source`/`installed`/`live`); observable failure oracle'],
|
|
22
|
+
];
|
|
23
|
+
const IMPLEMENTATION_READINESS_CLAUSES = {
|
|
24
|
+
noInvention: 'Before implementation handoff, apply the **no-invention gate**: if two implementations conform to the packet text yet can produce different externally observable output, state, error, security, or compatibility behavior, surface the missing choice as an explicit C1 or C2 question and block handoff.',
|
|
25
|
+
materialDefinition: 'A boundary is material when the task creates, changes, or depends on it and a different choice changes an external observation, security, durable data, compatibility, or proof reachability. Require only the matching material row; omit nonmaterial categories.',
|
|
26
|
+
exactBoundaryChoices: 'For every required row, name each listed choice exactly; labels such as “JSON”, “local path”, “locked”, or “timestamped” alone remain unresolved.',
|
|
27
|
+
proofPlanLines: [
|
|
28
|
+
'- Command: `<exact runnable command>`',
|
|
29
|
+
'- Named probe: <existing concrete probe/test/hook ID; never only a suite label>',
|
|
30
|
+
'- Reachability: <real entrypoint/caller at each `source`/`installed`/`live` level; `UNKNOWN` if unexecuted>',
|
|
31
|
+
'- Oracle: <externally observable success or failure>',
|
|
32
|
+
'- Counterexample: <material alternative behavior that must make this proof fail>',
|
|
33
|
+
'- Artifacts: <required artifact path plus digest algorithm/comparison rule, or explicitly ephemeral with cleanup rule>',
|
|
34
|
+
],
|
|
35
|
+
proofTrace: 'Trace `Command → Named probe → Reachability → Oracle`.',
|
|
36
|
+
namedProbeOwnership: 'Aggregate suites\nname the owning concrete probe.',
|
|
37
|
+
proofLevelSeparation: 'Proof at `source`/`installed`/`live` stays\nseparate; one level never promotes another.',
|
|
38
|
+
disposableTemplateControls: 'Run mutation or destructive\nnegative controls only on disposable copies under a verified temporary root,\nnever tracked worktree or canonical source bytes.',
|
|
39
|
+
disposableReviewControls: 'Run mutation or destructive negative controls only on disposable copies below a verified temporary root, never tracked worktree or canonical source bytes.',
|
|
40
|
+
failureSemantics: '`Crash` means abrupt unhandled termination before the claimed catch point; a catchable failure returns/raises an error or exits nonzero. Never use them interchangeably.',
|
|
41
|
+
privacyIdentifiers: 'Any privacy/security claim names the exact identifier surface at risk, such as an env var, header, path, token class, or field name; generic “sensitive data” is insufficient.',
|
|
42
|
+
freshReplay: 'After applying an accepted C2 finding, a fresh-context closure pass records and freshly replays its original counterexample after the repair under this exact review-log header:',
|
|
43
|
+
distinctRepairProof: '`Repaired at` cites the repair edit; `Proved at` must cite distinct evidence from the fresh replay, never the repair-edit citation.',
|
|
44
|
+
closureTransition: 'An accepted finding transitions `accepted → repaired → PASS|FAIL|UNKNOWN`.',
|
|
45
|
+
unknownBlocks: 'Only `PASS` closes it; `FAIL` remains open for the remaining paper-review round; `UNKNOWN` blocks implementation handoff.',
|
|
46
|
+
scopeReturnsToC1: 'A repair that adds user semantics or scope returns to C1.',
|
|
47
|
+
};
|
|
11
48
|
const REQUIRED_PAYLOAD = [
|
|
12
49
|
'bin/install.js',
|
|
13
50
|
'src/claude/migration-manifest.json',
|
|
@@ -33,9 +70,14 @@ const FORBIDDEN_PAYLOAD = [
|
|
|
33
70
|
/^src\/\.codex(?:\/|$)/,
|
|
34
71
|
];
|
|
35
72
|
const RUNTIMES = {
|
|
36
|
-
claude: {
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
claude: {
|
|
74
|
+
root: '.claude', rules: '.claude/hooks/rules.cjs', specs: '.claude/skills/specs',
|
|
75
|
+
manifest: '.claude/cafekit-manifest.json', templatesManifestPath: 'skills/specs/references/templates.md'
|
|
76
|
+
},
|
|
77
|
+
codex: {
|
|
78
|
+
root: '.codex', rules: '.codex/hooks/rules.cjs', specs: '.agents/skills/specs',
|
|
79
|
+
manifest: '.codex/cafekit-manifest.json', templatesManifestPath: '.agents/skills/specs/references/templates.md'
|
|
80
|
+
},
|
|
39
81
|
};
|
|
40
82
|
|
|
41
83
|
function npmPack(args, cwd) {
|
|
@@ -132,6 +174,434 @@ function runInstaller(installer, root, platforms, lang) {
|
|
|
132
174
|
env: { ...process.env, HOME: path.join(root, 'home'), PATH: '/usr/bin:/bin' },
|
|
133
175
|
});
|
|
134
176
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function markdownTableUnderHeading(content, heading) {
|
|
181
|
+
const lines = markdownSectionUnderHeading(content, heading).split('\n');
|
|
182
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith('|'));
|
|
183
|
+
if (tableStart < 0) return [];
|
|
184
|
+
const rows = [];
|
|
185
|
+
for (let index = tableStart; index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
|
|
186
|
+
const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
|
|
187
|
+
if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;
|
|
188
|
+
rows.push(cells);
|
|
189
|
+
}
|
|
190
|
+
return rows;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function markdownSectionUnderHeading(content, heading) {
|
|
194
|
+
const lines = String(content).split('\n');
|
|
195
|
+
const headingIndex = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
196
|
+
if (headingIndex < 0) return '';
|
|
197
|
+
const nextHeadingIndex = lines.findIndex(
|
|
198
|
+
(line, index) => index > headingIndex && /^##\s+/.test(line)
|
|
199
|
+
);
|
|
200
|
+
return lines.slice(headingIndex + 1, nextHeadingIndex < 0 ? undefined : nextHeadingIndex).join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeMarkdownWhitespace(content) {
|
|
204
|
+
return String(content).replace(/\s+/g, ' ').trim();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function markdownBetweenHeadings(content, startHeading, endHeading) {
|
|
208
|
+
const value = String(content);
|
|
209
|
+
const startMarker = `## ${startHeading}`;
|
|
210
|
+
const endMarker = `## ${endHeading}`;
|
|
211
|
+
const startIndex = value.indexOf(startMarker);
|
|
212
|
+
const endIndex = value.indexOf(endMarker, startIndex + startMarker.length);
|
|
213
|
+
if (startIndex < 0 || endIndex < 0) return '';
|
|
214
|
+
return value.slice(startIndex + startMarker.length, endIndex);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function implementationReadinessContractIssues(input) {
|
|
218
|
+
const keys = input && typeof input === 'object' && !Array.isArray(input)
|
|
219
|
+
? Object.keys(input).sort()
|
|
220
|
+
: [];
|
|
221
|
+
if (keys.join(',') !== 'review,templates'
|
|
222
|
+
|| typeof input.templates !== 'string' || typeof input.review !== 'string') {
|
|
223
|
+
throw new TypeError('implementation-readiness checker expects exactly templates and review UTF-8 strings');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const issues = new Set();
|
|
227
|
+
const authoring = markdownSectionUnderHeading(
|
|
228
|
+
input.templates,
|
|
229
|
+
'No-invention and conditional boundary contracts'
|
|
230
|
+
);
|
|
231
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.noInvention)) {
|
|
232
|
+
issues.add('no-invention');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const boundaryTable = markdownTableUnderHeading(
|
|
236
|
+
input.templates,
|
|
237
|
+
'No-invention and conditional boundary contracts'
|
|
238
|
+
);
|
|
239
|
+
const expectedBoundaryTable = [
|
|
240
|
+
['Boundary', 'Required contract when material'],
|
|
241
|
+
...IMPLEMENTATION_READINESS_BOUNDARY_ROWS,
|
|
242
|
+
];
|
|
243
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.materialDefinition)
|
|
244
|
+
|| !authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices)
|
|
245
|
+
|| IMPLEMENTATION_READINESS_BOUNDARY_ROWS.length !== 6
|
|
246
|
+
|| boundaryTable.length !== expectedBoundaryTable.length
|
|
247
|
+
|| JSON.stringify(boundaryTable) !== JSON.stringify(expectedBoundaryTable)) {
|
|
248
|
+
issues.add('boundary-contract');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const proof = markdownSectionUnderHeading(input.templates, 'Verification Plan');
|
|
252
|
+
const proofPlanLines = proof.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '));
|
|
253
|
+
const normalizedProofContract = normalizeMarkdownWhitespace(markdownBetweenHeadings(
|
|
254
|
+
input.templates,
|
|
255
|
+
'Verification Plan',
|
|
256
|
+
'Canonical inline Receipt'
|
|
257
|
+
));
|
|
258
|
+
const normalizedProofClauses = [
|
|
259
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofTrace,
|
|
260
|
+
IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
261
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
262
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
|
|
263
|
+
].map(normalizeMarkdownWhitespace);
|
|
264
|
+
const reviewNegativeControls = normalizeMarkdownWhitespace(
|
|
265
|
+
markdownSectionUnderHeading(input.review, 'B2 — fresh-context red team')
|
|
266
|
+
);
|
|
267
|
+
if (JSON.stringify(proofPlanLines) !== JSON.stringify(IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines)
|
|
268
|
+
|| normalizedProofClauses.some((clause) => !normalizedProofContract.includes(clause))
|
|
269
|
+
|| !reviewNegativeControls.includes(normalizeMarkdownWhitespace(
|
|
270
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls
|
|
271
|
+
))) {
|
|
272
|
+
issues.add('proof-chain');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const failureGuidance = markdownSectionUnderHeading(input.templates, 'Twelve edge-case dimensions');
|
|
276
|
+
if (!failureGuidance.includes(IMPLEMENTATION_READINESS_CLAUSES.failureSemantics)) {
|
|
277
|
+
issues.add('failure-semantics');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const evidenceRules = markdownSectionUnderHeading(input.review, 'B1 — evidence rule');
|
|
281
|
+
if (!evidenceRules.includes(IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers)) {
|
|
282
|
+
issues.add('privacy-identifiers');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const closure = markdownSectionUnderHeading(input.review, 'Accepted-repair closure');
|
|
286
|
+
const normalizedClosure = normalizeMarkdownWhitespace(closure);
|
|
287
|
+
const closureHeader = markdownTableUnderHeading(input.review, 'Accepted-repair closure')[0] || [];
|
|
288
|
+
if (JSON.stringify(closureHeader) !== JSON.stringify([
|
|
289
|
+
'ID', 'Decision', 'Original counterexample', 'Repaired at', 'Proved at', 'Replay', 'Closure',
|
|
290
|
+
])
|
|
291
|
+
|| !normalizedClosure.includes(normalizeMarkdownWhitespace(IMPLEMENTATION_READINESS_CLAUSES.freshReplay))
|
|
292
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof)
|
|
293
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.closureTransition)
|
|
294
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks)
|
|
295
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.scopeReturnsToC1)) {
|
|
296
|
+
issues.add('repair-closure');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return [...issues].sort();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function installedSpecsReadinessIssues(project, installedRoot, expectedRelative) {
|
|
303
|
+
assert.equal(path.isAbsolute(project), true, 'project root must be absolute');
|
|
304
|
+
assert.equal(path.isAbsolute(installedRoot), true, 'installed Specs root must be absolute');
|
|
305
|
+
const canonicalProject = fs.realpathSync(project);
|
|
306
|
+
const canonicalInstalled = fs.realpathSync(installedRoot);
|
|
307
|
+
const relative = path.relative(canonicalProject, canonicalInstalled);
|
|
308
|
+
assert.ok(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative), 'installed Specs root must stay inside project');
|
|
309
|
+
assert.equal(canonicalInstalled, fs.realpathSync(path.join(canonicalProject, expectedRelative)), 'checker must use the native installed Specs root');
|
|
310
|
+
return implementationReadinessContractIssues({
|
|
311
|
+
templates: fs.readFileSync(path.join(canonicalInstalled, 'references/templates.md'), 'utf8'),
|
|
312
|
+
review: fs.readFileSync(path.join(canonicalInstalled, 'references/review.md'), 'utf8'),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function assertInstalledSpecsReadiness(project, platform, expected = []) {
|
|
317
|
+
const runtime = RUNTIMES[platform];
|
|
318
|
+
assert.ok(runtime, `unknown platform: ${platform}`);
|
|
319
|
+
const installedRoot = path.join(project, runtime.specs);
|
|
320
|
+
const issues = installedSpecsReadinessIssues(project, installedRoot, runtime.specs);
|
|
321
|
+
assert.deepEqual(issues, expected, `${platform} installed Specs readiness issues`);
|
|
322
|
+
return issues;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function sha256(content) {
|
|
326
|
+
return createHash('sha256').update(content).digest('hex');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function assertContainedRealpath(root, target, label) {
|
|
330
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
331
|
+
const canonicalTarget = fs.realpathSync(target);
|
|
332
|
+
const relative = path.relative(canonicalRoot, canonicalTarget);
|
|
333
|
+
assert.ok(
|
|
334
|
+
relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative),
|
|
335
|
+
`${label} must stay inside ${canonicalRoot}`
|
|
336
|
+
);
|
|
337
|
+
return canonicalTarget;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function installedMutationContext(tempRoot, project, platform, packageRoot = PACKAGE_ROOT) {
|
|
341
|
+
const runtime = RUNTIMES[platform];
|
|
342
|
+
assert.ok(runtime, `unknown platform: ${platform}`);
|
|
343
|
+
const canonicalTempRoot = assertContainedRealpath(os.tmpdir(), tempRoot, `${platform} mkdtemp root`);
|
|
344
|
+
assert.match(path.basename(canonicalTempRoot), /^cafekit-package-matrix-/, `${platform} mutation root must be the verified matrix mkdtemp`);
|
|
345
|
+
const canonicalProject = assertContainedRealpath(canonicalTempRoot, project, `${platform} packed project`);
|
|
346
|
+
const nativeInstalledRoot = path.join(canonicalProject, runtime.specs);
|
|
347
|
+
const canonicalInstalledRoot = assertContainedRealpath(
|
|
348
|
+
canonicalProject,
|
|
349
|
+
nativeInstalledRoot,
|
|
350
|
+
`${platform} native installed Specs root`
|
|
351
|
+
);
|
|
352
|
+
const installedRelatives = {
|
|
353
|
+
templates: 'references/templates.md',
|
|
354
|
+
review: 'references/review.md',
|
|
355
|
+
};
|
|
356
|
+
const installedPaths = {
|
|
357
|
+
templates: path.join(nativeInstalledRoot, installedRelatives.templates),
|
|
358
|
+
review: path.join(nativeInstalledRoot, installedRelatives.review),
|
|
359
|
+
};
|
|
360
|
+
const sourcePaths = {
|
|
361
|
+
templates: path.join(packageRoot, 'src/claude/skills/specs/references/templates.md'),
|
|
362
|
+
review: path.join(packageRoot, 'src/claude/skills/specs/references/review.md'),
|
|
363
|
+
};
|
|
364
|
+
for (const source of Object.keys(sourcePaths)) sourcePaths[source] = fs.realpathSync(sourcePaths[source]);
|
|
365
|
+
const sourceHashes = Object.fromEntries(
|
|
366
|
+
Object.entries(sourcePaths).map(([source, sourcePath]) => [source, sha256(fs.readFileSync(sourcePath))])
|
|
367
|
+
);
|
|
368
|
+
const assertSourceHashes = (stage) => {
|
|
369
|
+
for (const source of Object.keys(sourcePaths)) {
|
|
370
|
+
assert.equal(
|
|
371
|
+
sha256(fs.readFileSync(sourcePaths[source])),
|
|
372
|
+
sourceHashes[source],
|
|
373
|
+
`${platform} PACKAGE_ROOT ${source} SHA changed ${stage}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const assertSafeInstalledTarget = (source, stage) => {
|
|
378
|
+
assert.ok(Object.hasOwn(installedPaths, source), `${platform} unknown installed mutation source: ${source}`);
|
|
379
|
+
assertSourceHashes(`before ${stage}`);
|
|
380
|
+
|
|
381
|
+
const currentTempRoot = assertContainedRealpath(os.tmpdir(), tempRoot, `${platform} ${stage} mkdtemp root`);
|
|
382
|
+
assert.equal(currentTempRoot, canonicalTempRoot, `${platform} ${stage} mutation root changed`);
|
|
383
|
+
const currentProject = assertContainedRealpath(currentTempRoot, project, `${platform} ${stage} packed project`);
|
|
384
|
+
assert.equal(currentProject, canonicalProject, `${platform} ${stage} packed project changed`);
|
|
385
|
+
const currentInstalledRoot = assertContainedRealpath(
|
|
386
|
+
currentProject,
|
|
387
|
+
nativeInstalledRoot,
|
|
388
|
+
`${platform} ${stage} native installed Specs root`
|
|
389
|
+
);
|
|
390
|
+
assert.equal(currentInstalledRoot, canonicalInstalledRoot, `${platform} ${stage} native installed Specs root changed`);
|
|
391
|
+
|
|
392
|
+
const target = installedPaths[source];
|
|
393
|
+
const targetMetadata = fs.lstatSync(target);
|
|
394
|
+
assert.equal(
|
|
395
|
+
targetMetadata.isSymbolicLink(),
|
|
396
|
+
false,
|
|
397
|
+
`${platform} ${stage} target must be a regular non-symlink file`
|
|
398
|
+
);
|
|
399
|
+
assert.equal(targetMetadata.isFile(), true, `${platform} ${stage} target must be a regular non-symlink file`);
|
|
400
|
+
const canonicalTarget = assertContainedRealpath(
|
|
401
|
+
currentInstalledRoot,
|
|
402
|
+
target,
|
|
403
|
+
`${platform} ${stage} installed ${source} mutation target`
|
|
404
|
+
);
|
|
405
|
+
assert.equal(
|
|
406
|
+
assertContainedRealpath(currentProject, target, `${platform} ${stage} disposable project target`),
|
|
407
|
+
canonicalTarget
|
|
408
|
+
);
|
|
409
|
+
assert.equal(
|
|
410
|
+
canonicalTarget,
|
|
411
|
+
path.join(currentInstalledRoot, installedRelatives[source]),
|
|
412
|
+
`${platform} ${stage} target must remain at its exact native installed path`
|
|
413
|
+
);
|
|
414
|
+
assert.notEqual(canonicalTarget, sourcePaths[source], `${platform} ${stage} target must not be package source`);
|
|
415
|
+
|
|
416
|
+
const installedIdentity = fs.statSync(target);
|
|
417
|
+
const sourceIdentity = fs.statSync(sourcePaths[source]);
|
|
418
|
+
assert.ok(
|
|
419
|
+
installedIdentity.dev !== sourceIdentity.dev || installedIdentity.ino !== sourceIdentity.ino,
|
|
420
|
+
`${platform} ${stage} target must not share (dev, ino) with package source`
|
|
421
|
+
);
|
|
422
|
+
return target;
|
|
423
|
+
};
|
|
424
|
+
const writeInstalled = (source, content, stage) => {
|
|
425
|
+
const target = assertSafeInstalledTarget(source, stage);
|
|
426
|
+
fs.writeFileSync(target, content);
|
|
427
|
+
assertSourceHashes(`immediately after ${stage}`);
|
|
428
|
+
};
|
|
429
|
+
for (const source of Object.keys(installedPaths)) {
|
|
430
|
+
assertSafeInstalledTarget(source, `${source} context creation`);
|
|
431
|
+
}
|
|
432
|
+
return { installedPaths, assertSafeInstalledTarget, writeInstalled, assertSourceHashes };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
test('installed mutation guard rejects post-context symlink and hardlink before source bytes change', () => {
|
|
436
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-package-matrix-'));
|
|
437
|
+
const trackedSources = {
|
|
438
|
+
templates: path.join(PACKAGE_ROOT, 'src/claude/skills/specs/references/templates.md'),
|
|
439
|
+
review: path.join(PACKAGE_ROOT, 'src/claude/skills/specs/references/review.md'),
|
|
440
|
+
};
|
|
441
|
+
const trackedHashes = Object.fromEntries(
|
|
442
|
+
Object.entries(trackedSources).map(([source, sourcePath]) => [source, sha256(fs.readFileSync(sourcePath))])
|
|
443
|
+
);
|
|
444
|
+
try {
|
|
445
|
+
for (const topology of ['symlink', 'hardlink']) {
|
|
446
|
+
const packageRoot = path.join(root, `${topology}-package-source`);
|
|
447
|
+
const project = path.join(root, `${topology}-project`);
|
|
448
|
+
const sourceDirectory = path.join(packageRoot, 'src/claude/skills/specs/references');
|
|
449
|
+
const installedDirectory = path.join(project, RUNTIMES.claude.specs, 'references');
|
|
450
|
+
fs.mkdirSync(sourceDirectory, { recursive: true });
|
|
451
|
+
fs.mkdirSync(installedDirectory, { recursive: true });
|
|
452
|
+
for (const file of ['templates.md', 'review.md']) {
|
|
453
|
+
fs.writeFileSync(path.join(sourceDirectory, file), `${topology} disposable source ${file}\n`);
|
|
454
|
+
fs.writeFileSync(path.join(installedDirectory, file), `${topology} disposable installed ${file}\n`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const context = installedMutationContext(root, project, 'claude', packageRoot);
|
|
458
|
+
const sourcePath = path.join(sourceDirectory, 'templates.md');
|
|
459
|
+
const target = context.installedPaths.templates;
|
|
460
|
+
const sourceBytes = fs.readFileSync(sourcePath);
|
|
461
|
+
const sourceHash = sha256(sourceBytes);
|
|
462
|
+
fs.unlinkSync(target);
|
|
463
|
+
if (topology === 'symlink') {
|
|
464
|
+
fs.symlinkSync(sourcePath, target);
|
|
465
|
+
assert.equal(fs.lstatSync(target).isSymbolicLink(), true, 'symlink control must change target topology');
|
|
466
|
+
} else {
|
|
467
|
+
fs.linkSync(sourcePath, target);
|
|
468
|
+
const sourceIdentity = fs.statSync(sourcePath);
|
|
469
|
+
const installedIdentity = fs.statSync(target);
|
|
470
|
+
assert.deepEqual(
|
|
471
|
+
[installedIdentity.dev, installedIdentity.ino],
|
|
472
|
+
[sourceIdentity.dev, sourceIdentity.ino],
|
|
473
|
+
'hardlink control must share source identity'
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
assert.throws(
|
|
478
|
+
() => context.writeInstalled('templates', 'forbidden installed mutation\n', `${topology} regression`),
|
|
479
|
+
topology === 'symlink' ? /regular non-symlink file/ : /must not share \(dev, ino\)/
|
|
480
|
+
);
|
|
481
|
+
assert.deepEqual(fs.readFileSync(sourcePath), sourceBytes, `${topology} rejection must preserve source bytes`);
|
|
482
|
+
assert.equal(sha256(fs.readFileSync(sourcePath)), sourceHash, `${topology} rejection must preserve source SHA`);
|
|
483
|
+
context.assertSourceHashes(`after rejected ${topology} write`);
|
|
484
|
+
}
|
|
485
|
+
for (const [source, sourcePath] of Object.entries(trackedSources)) {
|
|
486
|
+
assert.equal(
|
|
487
|
+
sha256(fs.readFileSync(sourcePath)),
|
|
488
|
+
trackedHashes[source],
|
|
489
|
+
`tracked PACKAGE_ROOT ${source} SHA must remain unchanged`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
} finally {
|
|
493
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
function assertInstalledReadinessMutationsDetected(tempRoot, project, platform) {
|
|
498
|
+
const context = installedMutationContext(tempRoot, project, platform);
|
|
499
|
+
const baseline = Object.fromEntries(
|
|
500
|
+
Object.entries(context.installedPaths).map(([source, target]) => [source, fs.readFileSync(target, 'utf8')])
|
|
501
|
+
);
|
|
502
|
+
const apiBoundary = IMPLEMENTATION_READINESS_BOUNDARY_ROWS.find(([label]) => label === 'API/CLI');
|
|
503
|
+
assert.ok(apiBoundary, 'API/CLI boundary contract must exist');
|
|
504
|
+
const mutations = [
|
|
505
|
+
{
|
|
506
|
+
name: 'no-invention-blocking', issue: 'no-invention', source: 'templates',
|
|
507
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
|
|
508
|
+
to: 'Before implementation handoff, note ambiguous choices without blocking handoff.',
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: 'api-success-and-error-semantics', issue: 'boundary-contract', source: 'templates',
|
|
512
|
+
from: `| ${apiBoundary[0]} | ${apiBoundary[1]} |`,
|
|
513
|
+
to: `| ${apiBoundary[0]} | ${apiBoundary[1].replace('success output; ', '').replace('error/status/exit; ', '')} |`,
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'concrete-named-probe', issue: 'proof-chain', source: 'templates',
|
|
517
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[1],
|
|
518
|
+
to: '- Named probe: <suite label>',
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'artifact-path-and-digest-or-ephemeral', issue: 'proof-chain', source: 'templates',
|
|
522
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[5],
|
|
523
|
+
to: '- Artifacts: <required artifact path, or none>',
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: 'distinct-repair-and-proof-evidence', issue: 'repair-closure', source: 'review',
|
|
527
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof,
|
|
528
|
+
to: '`Repaired at` and `Proved at` may cite the same repair edit.',
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: 'crash-versus-catchable-failure', issue: 'failure-semantics', source: 'templates',
|
|
532
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.failureSemantics,
|
|
533
|
+
to: 'Crash and catchable failure both mean an error occurred.',
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'privacy-identifier-surface', issue: 'privacy-identifiers', source: 'review',
|
|
537
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers,
|
|
538
|
+
to: 'Any privacy/security claim names the sensitive data at risk.',
|
|
539
|
+
},
|
|
540
|
+
];
|
|
541
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
542
|
+
for (const mutation of mutations) {
|
|
543
|
+
const anchorIndex = baseline[mutation.source].indexOf(mutation.from);
|
|
544
|
+
assert.notEqual(anchorIndex, -1, `${platform} ${mutation.name} mutation anchor must exist`);
|
|
545
|
+
assert.equal(
|
|
546
|
+
baseline[mutation.source].indexOf(mutation.from, anchorIndex + mutation.from.length),
|
|
547
|
+
-1,
|
|
548
|
+
`${platform} ${mutation.name} mutation anchor must be unique`
|
|
549
|
+
);
|
|
550
|
+
const weakened = `${baseline[mutation.source].slice(0, anchorIndex)}${mutation.to}${baseline[mutation.source].slice(anchorIndex + mutation.from.length)}`;
|
|
551
|
+
try {
|
|
552
|
+
context.writeInstalled(mutation.source, weakened, `${mutation.name} mutation`);
|
|
553
|
+
assertInstalledSpecsReadiness(project, platform, [mutation.issue]);
|
|
554
|
+
} finally {
|
|
555
|
+
context.writeInstalled(mutation.source, baseline[mutation.source], `${mutation.name} restoration`);
|
|
556
|
+
}
|
|
557
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function assertPristineUpgradeAndUserPreservation(installer, tempRoot, project, platform) {
|
|
562
|
+
const runtime = RUNTIMES[platform];
|
|
563
|
+
const context = installedMutationContext(tempRoot, project, platform);
|
|
564
|
+
const templatesPath = context.installedPaths.templates;
|
|
565
|
+
const manifestPath = path.join(project, runtime.manifest);
|
|
566
|
+
const metadataPath = path.join(project, runtime.root, 'cafekit.json');
|
|
567
|
+
const baseline = fs.readFileSync(templatesPath, 'utf8');
|
|
568
|
+
const weakened = baseline.replace(NO_INVENTION_ANCHOR, '');
|
|
569
|
+
assert.notEqual(weakened, baseline, `${platform} old fixture mutation must apply`);
|
|
570
|
+
try {
|
|
571
|
+
context.writeInstalled('templates', weakened, 'pristine-upgrade mutation');
|
|
572
|
+
const oldManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
573
|
+
assert.ok(oldManifest.files[runtime.templatesManifestPath], `${platform} templates ownership record missing`);
|
|
574
|
+
oldManifest.files[runtime.templatesManifestPath] = {
|
|
575
|
+
...oldManifest.files[runtime.templatesManifestPath], sha256: sha256(weakened), version: OLD_FIXTURE_VERSION
|
|
576
|
+
};
|
|
577
|
+
writeJson(manifestPath, oldManifest);
|
|
578
|
+
const oldMetadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
579
|
+
oldMetadata.version = OLD_FIXTURE_VERSION;
|
|
580
|
+
writeJson(metadataPath, oldMetadata);
|
|
581
|
+
|
|
582
|
+
runInstaller(installer, project, [platform], null);
|
|
583
|
+
context.assertSourceHashes('immediately after pristine-upgrade installer');
|
|
584
|
+
context.assertSafeInstalledTarget('templates', 'after pristine-upgrade installer');
|
|
585
|
+
assert.equal(fs.readFileSync(templatesPath, 'utf8'), baseline, `${platform} pristine old gate must upgrade`);
|
|
586
|
+
assert.equal(JSON.parse(fs.readFileSync(metadataPath, 'utf8')).version, PACKAGE_VERSION);
|
|
587
|
+
const upgradedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
588
|
+
assert.equal(upgradedManifest.files[runtime.templatesManifestPath].sha256, sha256(baseline));
|
|
589
|
+
assert.equal(upgradedManifest.files[runtime.templatesManifestPath].version, PACKAGE_VERSION);
|
|
590
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
591
|
+
|
|
592
|
+
context.writeInstalled('templates', weakened, 'user-preservation mutation after installer');
|
|
593
|
+
const manifestBeforePreservedRerun = fs.readFileSync(manifestPath, 'utf8');
|
|
594
|
+
const preserved = runInstaller(installer, project, [platform], null);
|
|
595
|
+
context.assertSourceHashes('immediately after user-preservation installer');
|
|
596
|
+
context.assertSafeInstalledTarget('templates', 'after user-preservation installer');
|
|
597
|
+
assert.equal(fs.readFileSync(templatesPath, 'utf8'), weakened, `${platform} user edit must be preserved`);
|
|
598
|
+
assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBeforePreservedRerun, `${platform} user-modified manifest must stay unchanged`);
|
|
599
|
+
assert.match(`${preserved.stdout}\n${preserved.stderr}`, /preserved \(user-modified\): Skill: specs/);
|
|
600
|
+
assertInstalledSpecsReadiness(project, platform, ['no-invention']);
|
|
601
|
+
} finally {
|
|
602
|
+
context.writeInstalled('templates', baseline, 'upgrade and user-preservation restoration');
|
|
603
|
+
}
|
|
604
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
135
605
|
}
|
|
136
606
|
|
|
137
607
|
function createProvenanceFixture(root) {
|
|
@@ -303,29 +773,25 @@ function assertCombinedInstructionIsolation(root) {
|
|
|
303
773
|
const core = managedBlock(agents, '<!-- CAFEKIT CORE START -->', '<!-- CAFEKIT CORE END -->');
|
|
304
774
|
const claudeBlock = managedBlock(claude, '<!-- CAFEKIT CLAUDE START -->', '<!-- CAFEKIT CLAUDE END -->');
|
|
305
775
|
const codexBlock = managedBlock(agents, '<!-- CAFEKIT CODEX START -->', '<!-- CAFEKIT CODEX END -->');
|
|
306
|
-
const opencodeBlock = managedBlock(agents, '<!-- CAFEKIT OPENCODE START -->', '<!-- CAFEKIT OPENCODE END -->');
|
|
307
776
|
|
|
308
777
|
for (const [content, marker] of [
|
|
309
778
|
[agents, '<!-- CAFEKIT CORE START -->'],
|
|
310
779
|
[agents, '<!-- CAFEKIT CODEX START -->'],
|
|
311
|
-
[agents, '<!-- CAFEKIT OPENCODE START -->'],
|
|
312
780
|
[claude, '<!-- CAFEKIT CLAUDE START -->']
|
|
313
781
|
]) assert.equal((content.match(new RegExp(marker, 'g')) || []).length, 1);
|
|
314
782
|
|
|
315
|
-
assert.doesNotMatch(core, /Claude|Codex
|
|
316
|
-
assert.doesNotMatch(claudeBlock, /Codex
|
|
783
|
+
assert.doesNotMatch(core, /Claude|Codex|\.claude|\.codex|\/hapo:|\$hapo-/i);
|
|
784
|
+
assert.doesNotMatch(claudeBlock, /Codex|\.codex|\$hapo-/i);
|
|
317
785
|
assert.match(codexBlock, /native project instruction surface is root `AGENTS\.md`/);
|
|
318
|
-
assert.match(opencodeBlock, /native project instruction surface is root `AGENTS\.md`/);
|
|
319
786
|
assert.match(agents, /shared-root trade-off is intentional/);
|
|
320
787
|
// H5 ownership/ignore contract
|
|
321
788
|
assert.match(core, /runtime-neutral/i);
|
|
322
789
|
assert.match(core, /fail-safe/i);
|
|
323
790
|
assert.match(codexBlock, /owned by Codex/i);
|
|
324
|
-
assert.match(codexBlock, /ignore this entire Codex block/i);
|
|
325
|
-
assert.
|
|
326
|
-
assert.match(opencodeBlock, /ignore this entire OpenCode block/i);
|
|
791
|
+
assert.match(codexBlock, /If you are Claude Code or any other runtime, ignore this entire Codex block/i);
|
|
792
|
+
assert.doesNotMatch(codexBlock, /If you are Codex CLI or any other runtime, ignore this entire Codex block/i);
|
|
327
793
|
assert.doesNotMatch(core, /\$hapo-|hapo:/i);
|
|
328
|
-
assert.doesNotMatch(claudeBlock, /<!-- CAFEKIT
|
|
794
|
+
assert.doesNotMatch(claudeBlock, /<!-- CAFEKIT CODEX /);
|
|
329
795
|
}
|
|
330
796
|
function stableInstallSnapshot(root, platforms) {
|
|
331
797
|
const files = ['AGENTS.md', 'CLAUDE.md', '.gitignore'];
|
|
@@ -359,12 +825,6 @@ function assertHookLanguage(root, platform, lang, sessionId) {
|
|
|
359
825
|
assert.equal(runtimeJson.locale.responseLanguage, lang || null);
|
|
360
826
|
const rulesPath = path.join(root, runtime.rules);
|
|
361
827
|
assert.ok(fs.existsSync(rulesPath), `installed rules path missing: ${rulesPath}`);
|
|
362
|
-
if (platform === 'opencode') {
|
|
363
|
-
const plugin = fs.readFileSync(rulesPath, 'utf8');
|
|
364
|
-
assert.match(plugin, /if \(respondLang\)/);
|
|
365
|
-
assert.match(plugin, /Respond in \$\{respondLang\}/);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
828
|
const result = spawnSync(process.execPath, [rulesPath], {
|
|
369
829
|
cwd: root,
|
|
370
830
|
input: JSON.stringify({ cwd: root, session_id: sessionId }),
|
|
@@ -603,10 +1063,11 @@ test('entry accepts only the boolean true discriminator', () => {
|
|
|
603
1063
|
}
|
|
604
1064
|
|
|
605
1065
|
function taskProjection({ id, title, ownedPath, criterion, verificationRef, role, artifact = false }) {
|
|
1066
|
+
const artifactPath = `artifacts/${id}.json`;
|
|
606
1067
|
const anchors = typedAnchorTable([
|
|
607
1068
|
{ id: `A-${id}-01`, type: 'file', target: ownedPath, role: 'owner', access: 'write', action: 'modify' },
|
|
608
1069
|
...(artifact ? [{
|
|
609
|
-
id: `A-${id}-02`, type: 'artifact', target:
|
|
1070
|
+
id: `A-${id}-02`, type: 'artifact', target: artifactPath,
|
|
610
1071
|
role: 'verifier', access: 'write', action: 'create',
|
|
611
1072
|
}] : []),
|
|
612
1073
|
{
|
|
@@ -647,7 +1108,7 @@ ${anchors}
|
|
|
647
1108
|
- **Verification ref:** ${verificationRef}
|
|
648
1109
|
- **Task role:** ${role}
|
|
649
1110
|
- **Command:** \`node --test test/entry.test.js\`
|
|
650
|
-
- **Expected:** Exit code 0 and the criterion-specific state is observed
|
|
1111
|
+
- **Expected:** Exit code 0 and the criterion-specific state is observed.${artifact ? ` The artifact \`${artifactPath}\` must exist, and SHA-256 over its current bytes must match the recorded digest.` : ''}
|
|
651
1112
|
- **Negative path:** Invalid input returns the named rejected state.
|
|
652
1113
|
- **Reachability:** \`src/entry.js\` is reached by the installed test command.
|
|
653
1114
|
`;
|
|
@@ -658,7 +1119,7 @@ function createTaskFixture(paths, root, feature) {
|
|
|
658
1119
|
const fixture = materializeSpecState(paths, root, feature);
|
|
659
1120
|
fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection({ proof: true }));
|
|
660
1121
|
fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection({
|
|
661
|
-
expected: 'exit 0, status enabled, and verifier artifact `artifacts/R1-02.json`',
|
|
1122
|
+
expected: 'exit 0, status enabled, and verifier artifact `artifacts/R1-02.json` whose SHA-256 over current bytes matches the recorded digest',
|
|
662
1123
|
taskful: true,
|
|
663
1124
|
}));
|
|
664
1125
|
const taskOne = 'tasks/task-R1-01-implement.md';
|
|
@@ -695,7 +1156,7 @@ function createTaskFixture(paths, root, feature) {
|
|
|
695
1156
|
}, {
|
|
696
1157
|
criterion: 'R1.2', case_kind: 'failure',
|
|
697
1158
|
scenario: 'The verification command completes without producing its proof artifact.',
|
|
698
|
-
expected: 'The verification criterion remains unsatisfied until the
|
|
1159
|
+
expected: 'The verification criterion remains unsatisfied until `artifacts/R1-02.json` exists and its SHA-256 over current bytes matches the recorded digest.',
|
|
699
1160
|
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
700
1161
|
}]);
|
|
701
1162
|
return fixture;
|
|
@@ -857,9 +1318,8 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
857
1318
|
const cases = [
|
|
858
1319
|
{ name: 'claude', platforms: ['claude'] },
|
|
859
1320
|
{ name: 'codex', platforms: ['codex'] },
|
|
860
|
-
{ name: '
|
|
861
|
-
{ name: 'combined', platforms: ['claude', 'codex',
|
|
862
|
-
{ name: 'combined-rerun', platforms: ['claude', 'codex', 'opencode'], rerun: true },
|
|
1321
|
+
{ name: 'combined', platforms: ['claude', 'codex'] },
|
|
1322
|
+
{ name: 'combined-rerun', platforms: ['claude', 'codex'], rerun: true },
|
|
863
1323
|
];
|
|
864
1324
|
for (const matrixCase of cases) {
|
|
865
1325
|
for (const lang of [null, 'vi']) {
|
|
@@ -870,8 +1330,15 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
870
1330
|
assertHookLanguage(project, platform, lang, `${project}-${matrixCase.name}-${lang || 'none'}`);
|
|
871
1331
|
assertInstalledScripts(project, platform);
|
|
872
1332
|
assertTransforms(project, platform);
|
|
1333
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
1334
|
+
if (matrixCase.platforms.length === 1 && lang === null) {
|
|
1335
|
+
assertInstalledReadinessMutationsDetected(root, project, platform);
|
|
1336
|
+
assertPristineUpgradeAndUserPreservation(installer, root, project, platform);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (matrixCase.platforms.includes('claude') && matrixCase.platforms.includes('codex')) {
|
|
1340
|
+
assertCombinedInstructionIsolation(project);
|
|
873
1341
|
}
|
|
874
|
-
if (matrixCase.platforms.length === 3) assertCombinedInstructionIsolation(project);
|
|
875
1342
|
if (matrixCase.rerun) {
|
|
876
1343
|
fs.appendFileSync(path.join(project, 'AGENTS.md'), '\n## User matrix note\nKeep this exact.\n');
|
|
877
1344
|
fs.appendFileSync(path.join(project, 'CLAUDE.md'), '\n## User Claude matrix note\nKeep this exact.\n');
|
|
@@ -881,6 +1348,7 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
881
1348
|
const changed = [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
882
1349
|
.filter((file) => !before[file] || !after[file] || !before[file].equals(after[file]));
|
|
883
1350
|
assert.deepEqual(changed, [], `${matrixCase.name} rerun must be byte-idempotent`);
|
|
1351
|
+
for (const platform of matrixCase.platforms) assertInstalledSpecsReadiness(project, platform);
|
|
884
1352
|
assertCombinedInstructionIsolation(project);
|
|
885
1353
|
assert.match(fs.readFileSync(path.join(project, 'AGENTS.md'), 'utf8'), /User matrix note\nKeep this exact\./);
|
|
886
1354
|
assert.match(fs.readFileSync(path.join(project, 'CLAUDE.md'), 'utf8'), /User Claude matrix note\nKeep this exact\./);
|
|
@@ -173,14 +173,14 @@ function buildEvolutionSpecFixture(root, name) {
|
|
|
173
173
|
// isolated root) -- reaches a real `released` generation 1 authority with no
|
|
174
174
|
// hand-crafted digests. Returns the bound instance and key commits/digests.
|
|
175
175
|
function publishOneReleasedGeneration(root) {
|
|
176
|
-
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
|
|
176
|
+
writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
|
|
177
177
|
schema_version: '1', verdict: 'PASS', blocking_count: 0, reviewed_criteria: ['R1.1'],
|
|
178
178
|
semantic_digest: cf.sha256Tag('bridge'), bootWindowBaseCommit: git(root, ['rev-parse', 'HEAD']).trim(),
|
|
179
179
|
written_at: new Date().toISOString(),
|
|
180
180
|
});
|
|
181
181
|
const baseCommit = git(root, ['rev-parse', 'HEAD']).trim();
|
|
182
182
|
const reviewReceiptDigest = cf.sha256Tag('seed-review-receipt');
|
|
183
|
-
writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), {
|
|
183
|
+
writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/spec.json'), {
|
|
184
184
|
validation: {
|
|
185
185
|
semantic_review_history: {
|
|
186
186
|
entries: [{ sequence: 0, review_epoch: 0, verdict: 'PASS', review_receipt_digest: reviewReceiptDigest, semantic_digest: cf.sha256Tag('feature-digest') }],
|