@haposoft/cafekit 0.16.0-rc.7 → 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/bin/__tests__/codex-hooks.test.js +245 -0
- package/bin/__tests__/codex-native.test.js +463 -20
- package/bin/__tests__/package-inventory.test.js +484 -6
- package/package.json +1 -1
- package/src/claude/agents/spec-maker.md +14 -8
- package/src/claude/hooks/__tests__/spec-gate.test.js +77 -1
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/spec-state.cjs +30 -3
- package/src/claude/scripts/spec-receipt.cjs +76 -21
- package/src/claude/scripts/spec-resolver.cjs +144 -4
- package/src/claude/skills/specs/SKILL.md +17 -7
- package/src/claude/skills/specs/references/review.md +15 -0
- package/src/claude/skills/specs/references/templates.md +49 -20
- package/src/codex/hooks/spec-state.cjs +32 -4
|
@@ -29,6 +29,39 @@ const MANIFEST = JSON.parse(
|
|
|
29
29
|
fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8')
|
|
30
30
|
);
|
|
31
31
|
const SPECS_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/specs');
|
|
32
|
+
const IMPLEMENTATION_READINESS_BOUNDARY_ROWS = [
|
|
33
|
+
['API/CLI', 'entrypoint/route or command grammar; identity/auth; input/default/normalization; success output; error/status/exit; duplicate/retry/idempotency; compatibility'],
|
|
34
|
+
['Schema', 'version; exact keys/nesting/types; required/optional; enum/format/bounds/cardinality; unknown-field behavior; compatibility/migration'],
|
|
35
|
+
['State/concurrency', 'initial/terminal states; event + guard + effect + next + error; ordering; duplicate/retry; writer/lock acquire/contention/release; rollback/recovery'],
|
|
36
|
+
['Filesystem/security', 'authoritative root; trusted/untrusted segment grammar; lexical + canonical containment and symlink policy; flags/mode; temp/rename/fsync; lock/stale reclaim; crash cleanup'],
|
|
37
|
+
['Time/retention', 'clock source; unit/precision/timezone; endpoints and inclusion/comparator; anomaly behavior; expiry/purge/recovery'],
|
|
38
|
+
['Integration/proof', 'caller; export/registration; packaging/config; native path/consumer; proof level (`source`/`installed`/`live`); observable failure oracle']
|
|
39
|
+
];
|
|
40
|
+
const IMPLEMENTATION_READINESS_CLAUSES = {
|
|
41
|
+
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.',
|
|
42
|
+
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.',
|
|
43
|
+
exactBoundaryChoices: 'For every required row, name each listed choice exactly; labels such as “JSON”, “local path”, “locked”, or “timestamped” alone remain unresolved.',
|
|
44
|
+
proofPlanLines: [
|
|
45
|
+
'- Command: `<exact runnable command>`',
|
|
46
|
+
'- Named probe: <existing concrete probe/test/hook ID; never only a suite label>',
|
|
47
|
+
'- Reachability: <real entrypoint/caller at each `source`/`installed`/`live` level; `UNKNOWN` if unexecuted>',
|
|
48
|
+
'- Oracle: <externally observable success or failure>',
|
|
49
|
+
'- Counterexample: <material alternative behavior that must make this proof fail>',
|
|
50
|
+
'- Artifacts: <required artifact path plus digest algorithm/comparison rule, or explicitly ephemeral with cleanup rule>'
|
|
51
|
+
],
|
|
52
|
+
proofTrace: 'Trace `Command → Named probe → Reachability → Oracle`.',
|
|
53
|
+
namedProbeOwnership: 'Aggregate suites\nname the owning concrete probe.',
|
|
54
|
+
proofLevelSeparation: 'Proof at `source`/`installed`/`live` stays\nseparate; one level never promotes another.',
|
|
55
|
+
disposableTemplateControls: 'Run mutation or destructive\nnegative controls only on disposable copies under a verified temporary root,\nnever tracked worktree or canonical source bytes.',
|
|
56
|
+
disposableReviewControls: 'Run mutation or destructive negative controls only on disposable copies below a verified temporary root, never tracked worktree or canonical source bytes.',
|
|
57
|
+
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.',
|
|
58
|
+
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.',
|
|
59
|
+
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:',
|
|
60
|
+
distinctRepairProof: '`Repaired at` cites the repair edit; `Proved at` must cite distinct evidence from the fresh replay, never the repair-edit citation.',
|
|
61
|
+
closureTransition: 'An accepted finding transitions `accepted → repaired → PASS|FAIL|UNKNOWN`.',
|
|
62
|
+
unknownBlocks: 'Only `PASS` closes it; `FAIL` remains open for the remaining paper-review round; `UNKNOWN` blocks implementation handoff.',
|
|
63
|
+
scopeReturnsToC1: 'A repair that adds user semantics or scope returns to C1.'
|
|
64
|
+
};
|
|
32
65
|
const V3_SPECS_BUNDLE = [
|
|
33
66
|
'SKILL.md',
|
|
34
67
|
'references/review.md',
|
|
@@ -159,8 +192,122 @@ function assertInstalledVerificationModel(grounderPath, designTemplate) {
|
|
|
159
192
|
}
|
|
160
193
|
}
|
|
161
194
|
|
|
162
|
-
function
|
|
195
|
+
function markdownSection(content, heading) {
|
|
196
|
+
const lines = String(content).split('\n');
|
|
197
|
+
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
198
|
+
if (start < 0) return '';
|
|
199
|
+
const end = lines.findIndex((line, index) => index > start && /^##\s+/.test(line));
|
|
200
|
+
return lines.slice(start + 1, end < 0 ? undefined : end).join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function markdownTableUnderHeading(content, heading) {
|
|
204
|
+
const lines = markdownSection(content, heading).split('\n');
|
|
205
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith('|'));
|
|
206
|
+
if (tableStart < 0) return [];
|
|
207
|
+
const rows = [];
|
|
208
|
+
for (let index = tableStart; index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
|
|
209
|
+
const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
|
|
210
|
+
if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;
|
|
211
|
+
rows.push(cells);
|
|
212
|
+
}
|
|
213
|
+
return rows;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeMarkdownWhitespace(content) {
|
|
217
|
+
return String(content).replace(/\s+/g, ' ').trim();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function markdownBetweenHeadings(content, startHeading, endHeading) {
|
|
221
|
+
const value = String(content);
|
|
222
|
+
const startMarker = `## ${startHeading}`;
|
|
223
|
+
const endMarker = `## ${endHeading}`;
|
|
224
|
+
const startIndex = value.indexOf(startMarker);
|
|
225
|
+
const endIndex = value.indexOf(endMarker, startIndex + startMarker.length);
|
|
226
|
+
if (startIndex < 0 || endIndex < 0) return '';
|
|
227
|
+
return value.slice(startIndex + startMarker.length, endIndex);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function implementationReadinessIssues(templates, review) {
|
|
231
|
+
if (typeof templates !== 'string' || typeof review !== 'string') {
|
|
232
|
+
throw new TypeError('implementation-readiness checker expects templates and review UTF-8 strings');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const issues = new Set();
|
|
236
|
+
const authoring = markdownSection(templates, 'No-invention and conditional boundary contracts');
|
|
237
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.noInvention)) {
|
|
238
|
+
issues.add('no-invention');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const boundaryTable = markdownTableUnderHeading(
|
|
242
|
+
templates,
|
|
243
|
+
'No-invention and conditional boundary contracts'
|
|
244
|
+
);
|
|
245
|
+
const expectedBoundaryTable = [
|
|
246
|
+
['Boundary', 'Required contract when material'],
|
|
247
|
+
...IMPLEMENTATION_READINESS_BOUNDARY_ROWS
|
|
248
|
+
];
|
|
249
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.materialDefinition)
|
|
250
|
+
|| !authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices)
|
|
251
|
+
|| IMPLEMENTATION_READINESS_BOUNDARY_ROWS.length !== 6
|
|
252
|
+
|| boundaryTable.length !== expectedBoundaryTable.length
|
|
253
|
+
|| JSON.stringify(boundaryTable) !== JSON.stringify(expectedBoundaryTable)) {
|
|
254
|
+
issues.add('boundary-contract');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const proof = markdownSection(templates, 'Verification Plan');
|
|
258
|
+
const proofPlanLines = proof.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '));
|
|
259
|
+
const normalizedProofContract = normalizeMarkdownWhitespace(markdownBetweenHeadings(
|
|
260
|
+
templates,
|
|
261
|
+
'Verification Plan',
|
|
262
|
+
'Canonical inline Receipt'
|
|
263
|
+
));
|
|
264
|
+
const normalizedProofClauses = [
|
|
265
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofTrace,
|
|
266
|
+
IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
267
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
268
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls
|
|
269
|
+
].map(normalizeMarkdownWhitespace);
|
|
270
|
+
const reviewNegativeControls = normalizeMarkdownWhitespace(
|
|
271
|
+
markdownSection(review, 'B2 — fresh-context red team')
|
|
272
|
+
);
|
|
273
|
+
if (JSON.stringify(proofPlanLines) !== JSON.stringify(IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines)
|
|
274
|
+
|| normalizedProofClauses.some((clause) => !normalizedProofContract.includes(clause))
|
|
275
|
+
|| !reviewNegativeControls.includes(normalizeMarkdownWhitespace(
|
|
276
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls
|
|
277
|
+
))) {
|
|
278
|
+
issues.add('proof-chain');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const failureGuidance = markdownSection(templates, 'Twelve edge-case dimensions');
|
|
282
|
+
if (!failureGuidance.includes(IMPLEMENTATION_READINESS_CLAUSES.failureSemantics)) {
|
|
283
|
+
issues.add('failure-semantics');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const evidenceRules = markdownSection(review, 'B1 — evidence rule');
|
|
287
|
+
if (!evidenceRules.includes(IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers)) {
|
|
288
|
+
issues.add('privacy-identifiers');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const closure = markdownSection(review, 'Accepted-repair closure');
|
|
292
|
+
const normalizedClosure = normalizeMarkdownWhitespace(closure);
|
|
293
|
+
const closureHeader = markdownTableUnderHeading(review, 'Accepted-repair closure')[0] || [];
|
|
294
|
+
if (JSON.stringify(closureHeader) !== JSON.stringify([
|
|
295
|
+
'ID', 'Decision', 'Original counterexample', 'Repaired at', 'Proved at', 'Replay', 'Closure'
|
|
296
|
+
])
|
|
297
|
+
|| !normalizedClosure.includes(normalizeMarkdownWhitespace(IMPLEMENTATION_READINESS_CLAUSES.freshReplay))
|
|
298
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof)
|
|
299
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.closureTransition)
|
|
300
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks)
|
|
301
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.scopeReturnsToC1)) {
|
|
302
|
+
issues.add('repair-closure');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return [...issues].sort();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function authoringProjectionIssues(files) {
|
|
163
309
|
const issues = [];
|
|
310
|
+
issues.push(...implementationReadinessIssues(files.templates, files.review));
|
|
164
311
|
const foreignRuntimeBoundary = 'If you are Claude Code or any other runtime, ignore this entire Codex block';
|
|
165
312
|
if (!files.skill.includes('Task files are flat beside `plan.md`')
|
|
166
313
|
|| !files.skill.includes('Do not create a nested task directory.')) {
|
|
@@ -211,11 +358,93 @@ function installedAuthoringProjectionIssues(files) {
|
|
|
211
358
|
|| !files.codex.includes('C1') || !files.codex.includes('C2') || !files.codex.includes('C3')) {
|
|
212
359
|
issues.push('codex-process-v3');
|
|
213
360
|
}
|
|
214
|
-
return issues;
|
|
361
|
+
return [...new Set(issues)].sort();
|
|
215
362
|
}
|
|
216
363
|
|
|
217
|
-
function
|
|
218
|
-
|
|
364
|
+
function canonicalInstalledSpecsRoot(projectRoot, installedRoot) {
|
|
365
|
+
assert.equal(path.isAbsolute(projectRoot), true, 'project root must be absolute');
|
|
366
|
+
assert.equal(path.isAbsolute(installedRoot), true, 'installed Specs root must be absolute');
|
|
367
|
+
const canonicalProject = fs.realpathSync(projectRoot);
|
|
368
|
+
const canonicalInstalled = fs.realpathSync(installedRoot);
|
|
369
|
+
const relative = path.relative(canonicalProject, canonicalInstalled);
|
|
370
|
+
assert.ok(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative), 'installed Specs root must stay inside project');
|
|
371
|
+
assert.equal(
|
|
372
|
+
canonicalInstalled,
|
|
373
|
+
fs.realpathSync(path.join(canonicalProject, '.agents/skills/specs')),
|
|
374
|
+
'checker must read the native installed Codex Specs root'
|
|
375
|
+
);
|
|
376
|
+
return canonicalInstalled;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function readInstalledAuthoringProjection(projectRoot, installedRoot) {
|
|
380
|
+
const canonicalRoot = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
|
|
381
|
+
const read = (relative) => fs.readFileSync(path.join(canonicalRoot, relative), 'utf8');
|
|
382
|
+
return {
|
|
383
|
+
skill: read('SKILL.md'),
|
|
384
|
+
review: read('references/review.md'),
|
|
385
|
+
templates: read('references/templates.md'),
|
|
386
|
+
legacyTemplates: V3_SPECS_BUNDLE.filter((relative) => relative.startsWith('templates/')).map(read).join('\n'),
|
|
387
|
+
codex: [
|
|
388
|
+
fs.readFileSync(path.join(projectRoot, 'AGENTS.md'), 'utf8'),
|
|
389
|
+
fs.readFileSync(path.join(projectRoot, '.codex/rules/state-sync.md'), 'utf8')
|
|
390
|
+
].join('\n')
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function installedAuthoringProjectionIssues(projectRoot, installedRoot) {
|
|
395
|
+
return authoringProjectionIssues(readInstalledAuthoringProjection(projectRoot, installedRoot));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function assertInstalledMutationTarget(projectRoot, installedRoot, relative) {
|
|
399
|
+
const canonicalProject = fs.realpathSync(projectRoot);
|
|
400
|
+
const canonicalInstalled = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
|
|
401
|
+
const targetPath = path.join(canonicalInstalled, relative);
|
|
402
|
+
const targetLstat = fs.lstatSync(targetPath);
|
|
403
|
+
assert.equal(targetLstat.isSymbolicLink(), false, 'installed mutation target must not be a symlink');
|
|
404
|
+
assert.equal(targetLstat.isFile(), true, 'installed mutation target must be a regular file');
|
|
405
|
+
const canonicalTarget = fs.realpathSync(targetPath);
|
|
406
|
+
const installedRelative = path.relative(canonicalInstalled, canonicalTarget);
|
|
407
|
+
const projectRelative = path.relative(canonicalProject, canonicalTarget);
|
|
408
|
+
assert.ok(
|
|
409
|
+
installedRelative && installedRelative !== '..'
|
|
410
|
+
&& !installedRelative.startsWith(`..${path.sep}`)
|
|
411
|
+
&& !path.isAbsolute(installedRelative),
|
|
412
|
+
'installed mutation target must stay inside .agents/skills/specs'
|
|
413
|
+
);
|
|
414
|
+
assert.ok(
|
|
415
|
+
projectRelative && projectRelative !== '..'
|
|
416
|
+
&& !projectRelative.startsWith(`..${path.sep}`)
|
|
417
|
+
&& !path.isAbsolute(projectRelative),
|
|
418
|
+
'installed mutation target must stay inside the disposable project'
|
|
419
|
+
);
|
|
420
|
+
const canonicalSource = fs.realpathSync(path.join(SPECS_SOURCE_ROOT, relative));
|
|
421
|
+
assert.notEqual(
|
|
422
|
+
canonicalTarget,
|
|
423
|
+
canonicalSource,
|
|
424
|
+
'installed mutation target must not resolve to canonical source'
|
|
425
|
+
);
|
|
426
|
+
const targetStat = fs.statSync(canonicalTarget);
|
|
427
|
+
const sourceStat = fs.statSync(canonicalSource);
|
|
428
|
+
assert.notDeepEqual(
|
|
429
|
+
[targetStat.dev, targetStat.ino],
|
|
430
|
+
[sourceStat.dev, sourceStat.ino],
|
|
431
|
+
'installed mutation target must not share an inode with canonical source'
|
|
432
|
+
);
|
|
433
|
+
return canonicalTarget;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function assertCanonicalSpecsSourceBytesUnchanged(sourceBytes) {
|
|
437
|
+
for (const [relative, expected] of sourceBytes) {
|
|
438
|
+
assert.deepEqual(
|
|
439
|
+
fs.readFileSync(path.join(SPECS_SOURCE_ROOT, relative)),
|
|
440
|
+
expected,
|
|
441
|
+
`canonical Specs source bytes changed during installed mutation: ${relative}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function assertInstalledAuthoringProjection(root, installedRoot) {
|
|
447
|
+
canonicalInstalledSpecsRoot(root, installedRoot);
|
|
219
448
|
const relativeFiles = (directory) => allFiles(directory, () => true)
|
|
220
449
|
.map((file) => path.relative(directory, file).split(path.sep).join('/'))
|
|
221
450
|
.sort();
|
|
@@ -232,21 +461,220 @@ function assertInstalledAuthoringProjection(root) {
|
|
|
232
461
|
assert.equal(fs.existsSync(path.join(installedRoot, relative)), false, `obsolete Specs file installed: ${relative}`);
|
|
233
462
|
}
|
|
234
463
|
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
464
|
+
const files = readInstalledAuthoringProjection(root, installedRoot);
|
|
465
|
+
const sourceBytes = new Map(V3_SPECS_BUNDLE.map((relative) => [
|
|
466
|
+
relative,
|
|
467
|
+
fs.readFileSync(path.join(SPECS_SOURCE_ROOT, relative))
|
|
468
|
+
]));
|
|
469
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), []);
|
|
470
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
471
|
+
|
|
472
|
+
if (process.platform !== 'win32') {
|
|
473
|
+
const relative = 'references/templates.md';
|
|
474
|
+
const installedPath = path.join(installedRoot, relative);
|
|
475
|
+
const backupPath = `${installedPath}.cafekit-backup`;
|
|
476
|
+
const sourcePath = path.join(SPECS_SOURCE_ROOT, relative);
|
|
477
|
+
fs.renameSync(installedPath, backupPath);
|
|
478
|
+
try {
|
|
479
|
+
fs.linkSync(sourcePath, installedPath);
|
|
480
|
+
assert.throws(
|
|
481
|
+
() => assertInstalledMutationTarget(root, installedRoot, relative),
|
|
482
|
+
/must not share an inode with canonical source/
|
|
483
|
+
);
|
|
484
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
485
|
+
} finally {
|
|
486
|
+
try { fs.unlinkSync(installedPath); } catch { /* best-effort disposable cleanup */ }
|
|
487
|
+
fs.renameSync(backupPath, installedPath);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const boundaryMutation = (name, boundary, replacements) => {
|
|
492
|
+
const row = IMPLEMENTATION_READINESS_BOUNDARY_ROWS.find(([label]) => label === boundary);
|
|
493
|
+
assert.ok(row, `${name} references unknown boundary ${boundary}`);
|
|
494
|
+
let weakened = row[1];
|
|
495
|
+
for (const [from, to] of replacements) {
|
|
496
|
+
const next = weakened.replace(from, to);
|
|
497
|
+
assert.notEqual(next, weakened, `${name} weakening anchor must exist in ${boundary}`);
|
|
498
|
+
weakened = next;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
name,
|
|
502
|
+
issue: 'boundary-contract',
|
|
503
|
+
relative: 'references/templates.md',
|
|
504
|
+
from: `| ${row[0]} | ${row[1]} |`,
|
|
505
|
+
to: `| ${row[0]} | ${weakened} |`
|
|
506
|
+
};
|
|
248
507
|
};
|
|
249
|
-
|
|
508
|
+
|
|
509
|
+
const readinessMutations = [
|
|
510
|
+
{
|
|
511
|
+
name: 'no-invention-blocking',
|
|
512
|
+
issue: 'no-invention',
|
|
513
|
+
relative: 'references/templates.md',
|
|
514
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
|
|
515
|
+
to: 'Before implementation handoff, note ambiguous choices without blocking handoff.'
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: 'material-boundary-definition',
|
|
519
|
+
issue: 'boundary-contract',
|
|
520
|
+
relative: 'references/templates.md',
|
|
521
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.materialDefinition,
|
|
522
|
+
to: 'A boundary is material when it seems relevant to the task.'
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: 'exact-boundary-choices',
|
|
526
|
+
issue: 'boundary-contract',
|
|
527
|
+
relative: 'references/templates.md',
|
|
528
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices,
|
|
529
|
+
to: 'For every required row, describe the listed choices generally.'
|
|
530
|
+
},
|
|
531
|
+
boundaryMutation('api-success-and-error-semantics', 'API/CLI', [
|
|
532
|
+
['success output; ', ''],
|
|
533
|
+
['error/status/exit; ', '']
|
|
534
|
+
]),
|
|
535
|
+
boundaryMutation('schema-shape-and-unknown-fields', 'Schema', [
|
|
536
|
+
['exact keys/nesting/types; ', ''],
|
|
537
|
+
['unknown-field behavior; ', '']
|
|
538
|
+
]),
|
|
539
|
+
boundaryMutation('state-lock-lifecycle', 'State/concurrency', [
|
|
540
|
+
['writer/lock acquire/contention/release', 'writer/lock']
|
|
541
|
+
]),
|
|
542
|
+
boundaryMutation('filesystem-segment-grammar', 'Filesystem/security', [
|
|
543
|
+
['trusted/untrusted segment grammar; ', '']
|
|
544
|
+
]),
|
|
545
|
+
boundaryMutation('filesystem-stale-lock-reclaim', 'Filesystem/security', [
|
|
546
|
+
['lock/stale reclaim; ', '']
|
|
547
|
+
]),
|
|
548
|
+
boundaryMutation('retention-clock-and-endpoints', 'Time/retention', [
|
|
549
|
+
['clock source; ', ''],
|
|
550
|
+
['unit/precision/timezone; ', ''],
|
|
551
|
+
['endpoints and inclusion/comparator; ', '']
|
|
552
|
+
]),
|
|
553
|
+
boundaryMutation('proof-level-partition', 'Integration/proof', [
|
|
554
|
+
['proof level (`source`/`installed`/`live`)', 'proof level']
|
|
555
|
+
]),
|
|
556
|
+
{
|
|
557
|
+
name: 'concrete-named-probe',
|
|
558
|
+
issue: 'proof-chain',
|
|
559
|
+
relative: 'references/templates.md',
|
|
560
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[1],
|
|
561
|
+
to: '- Named probe: <suite label>'
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'aggregate-suite-probe-owner',
|
|
565
|
+
issue: 'proof-chain',
|
|
566
|
+
relative: 'references/templates.md',
|
|
567
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
568
|
+
to: 'Aggregate suites may cite only the suite label.'
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: 'reachability-levels',
|
|
572
|
+
issue: 'proof-chain',
|
|
573
|
+
relative: 'references/templates.md',
|
|
574
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[2],
|
|
575
|
+
to: '- Reachability: <entrypoint or consumer>'
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'proof-level-separation',
|
|
579
|
+
issue: 'proof-chain',
|
|
580
|
+
relative: 'references/templates.md',
|
|
581
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
582
|
+
to: 'Proof may be promoted between source, installed, and live levels.'
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: 'disposable-template-negative-controls',
|
|
586
|
+
issue: 'proof-chain',
|
|
587
|
+
relative: 'references/templates.md',
|
|
588
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
|
|
589
|
+
to: 'Run mutation or destructive negative controls against the available project copy.'
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: 'disposable-review-negative-controls',
|
|
593
|
+
issue: 'proof-chain',
|
|
594
|
+
relative: 'references/review.md',
|
|
595
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls,
|
|
596
|
+
to: 'Run mutation or destructive negative controls against the available project copy.'
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: 'artifact-path-and-digest-or-ephemeral',
|
|
600
|
+
issue: 'proof-chain',
|
|
601
|
+
relative: 'references/templates.md',
|
|
602
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[5],
|
|
603
|
+
to: '- Artifacts: <required artifact path, or none>'
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'proof-counterexample',
|
|
607
|
+
issue: 'proof-chain',
|
|
608
|
+
relative: 'references/templates.md',
|
|
609
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[4],
|
|
610
|
+
to: '- Counterexample: <example>'
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: 'repair-and-proof-columns',
|
|
614
|
+
issue: 'repair-closure',
|
|
615
|
+
relative: 'references/review.md',
|
|
616
|
+
from: '| ID | Decision | Original counterexample | Repaired at | Proved at | Replay | Closure |',
|
|
617
|
+
to: '| ID | Decision | Original counterexample | Repaired at | Evidence | Replay | Closure |'
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'fresh-original-counterexample-replay',
|
|
621
|
+
issue: 'repair-closure',
|
|
622
|
+
relative: 'references/review.md',
|
|
623
|
+
from: 'After applying an accepted C2 finding, a fresh-context closure pass records and\nfreshly replays its original counterexample after the repair under this exact review-log header:',
|
|
624
|
+
to: 'After applying an accepted C2 finding, record the repair under this review-log header:'
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: 'distinct-repair-and-proof-evidence',
|
|
628
|
+
issue: 'repair-closure',
|
|
629
|
+
relative: 'references/review.md',
|
|
630
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof,
|
|
631
|
+
to: '`Repaired at` and `Proved at` may cite the same repair edit.'
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'unknown-blocks-handoff',
|
|
635
|
+
issue: 'repair-closure',
|
|
636
|
+
relative: 'references/review.md',
|
|
637
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks,
|
|
638
|
+
to: '`PASS` closes it; `FAIL` and `UNKNOWN` may continue to implementation handoff.'
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'crash-versus-catchable-failure',
|
|
642
|
+
issue: 'failure-semantics',
|
|
643
|
+
relative: 'references/templates.md',
|
|
644
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.failureSemantics,
|
|
645
|
+
to: 'Crash and catchable failure both mean an error occurred.'
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: 'privacy-identifier-surface',
|
|
649
|
+
issue: 'privacy-identifiers',
|
|
650
|
+
relative: 'references/review.md',
|
|
651
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers,
|
|
652
|
+
to: 'Any privacy/security claim names the sensitive data at risk.'
|
|
653
|
+
}
|
|
654
|
+
];
|
|
655
|
+
|
|
656
|
+
for (const { name, issue, relative, from, to } of readinessMutations) {
|
|
657
|
+
const target = assertInstalledMutationTarget(root, installedRoot, relative);
|
|
658
|
+
const installedBytes = fs.readFileSync(target, 'utf8');
|
|
659
|
+
const anchorIndex = installedBytes.indexOf(from);
|
|
660
|
+
assert.ok(anchorIndex >= 0, `${name} mutation anchor must exist in installed bytes`);
|
|
661
|
+
assert.equal(
|
|
662
|
+
installedBytes.indexOf(from, anchorIndex + from.length),
|
|
663
|
+
-1,
|
|
664
|
+
`${name} mutation anchor must be unique in installed bytes`
|
|
665
|
+
);
|
|
666
|
+
const weakened = `${installedBytes.slice(0, anchorIndex)}${to}${installedBytes.slice(anchorIndex + from.length)}`;
|
|
667
|
+
try {
|
|
668
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), weakened);
|
|
669
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
670
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [issue], name);
|
|
671
|
+
} finally {
|
|
672
|
+
fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), installedBytes);
|
|
673
|
+
}
|
|
674
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [], `${name} restore`);
|
|
675
|
+
assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
|
|
676
|
+
}
|
|
677
|
+
|
|
250
678
|
const mutations = [
|
|
251
679
|
['skill', (value) => value.replace('Task files are flat beside `plan.md`', 'Task files may be nested')],
|
|
252
680
|
['review', (value) => value.replace('list at 15', 'list without a cap')],
|
|
@@ -255,7 +683,7 @@ function assertInstalledAuthoringProjection(root) {
|
|
|
255
683
|
];
|
|
256
684
|
for (const [key, mutate] of mutations) {
|
|
257
685
|
const changed = { ...files, [key]: mutate(files[key]) };
|
|
258
|
-
assert.notDeepEqual(
|
|
686
|
+
assert.notDeepEqual(authoringProjectionIssues(changed), []);
|
|
259
687
|
}
|
|
260
688
|
}
|
|
261
689
|
|
|
@@ -715,7 +1143,20 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
715
1143
|
path.join(root, '.codex/scripts/spec-ground.cjs'),
|
|
716
1144
|
installedDesign
|
|
717
1145
|
);
|
|
718
|
-
|
|
1146
|
+
const installedSpecsRoot = path.join(root, '.agents/skills/specs');
|
|
1147
|
+
assert.throws(
|
|
1148
|
+
() => installedAuthoringProjectionIssues(root, '.agents/skills/specs'),
|
|
1149
|
+
/must be absolute/
|
|
1150
|
+
);
|
|
1151
|
+
assert.throws(
|
|
1152
|
+
() => installedAuthoringProjectionIssues(root, SPECS_SOURCE_ROOT),
|
|
1153
|
+
/must stay inside project/
|
|
1154
|
+
);
|
|
1155
|
+
assert.throws(
|
|
1156
|
+
() => installedAuthoringProjectionIssues(root, path.dirname(root)),
|
|
1157
|
+
/must stay inside project/
|
|
1158
|
+
);
|
|
1159
|
+
assertInstalledAuthoringProjection(root, installedSpecsRoot);
|
|
719
1160
|
|
|
720
1161
|
const installedTask = fs.readFileSync(
|
|
721
1162
|
path.join(root, '.agents/skills/specs/templates/task.md'), 'utf8'
|
|
@@ -845,6 +1286,7 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
845
1286
|
|
|
846
1287
|
const sameVersion = install(root);
|
|
847
1288
|
assert.equal(sameVersion.status, 0, `${sameVersion.stdout}\n${sameVersion.stderr}`);
|
|
1289
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
|
|
848
1290
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
849
1291
|
assert.equal(
|
|
850
1292
|
fs.existsSync(obsoleteSpecsRule),
|
|
@@ -859,6 +1301,7 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
|
|
|
859
1301
|
|
|
860
1302
|
const upgrade = install(root);
|
|
861
1303
|
assert.equal(upgrade.status, 0, `${upgrade.stdout}\n${upgrade.stderr}`);
|
|
1304
|
+
assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
|
|
862
1305
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
863
1306
|
assert.equal(
|
|
864
1307
|
fs.existsSync(obsoleteSpecsRule),
|