@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +531 -29
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +653 -185
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/installer-safety.test.js +39 -0
- package/bin/__tests__/opencode-spec-gate.test.js +24 -16
- package/bin/__tests__/package-inventory.test.js +705 -13
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2013 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +5 -2
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +143 -99
- package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
- package/src/claude/hooks/completion-authority-check.cjs +37 -288
- package/src/claude/hooks/completion-authority-state.cjs +10 -8
- package/src/claude/hooks/completion-authority.cjs +13 -7
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +99 -28
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/spec-gate.cjs +47 -117
- package/src/claude/hooks/spec-state.cjs +8 -2
- package/src/claude/migration-manifest.json +9 -1
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- 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 +290 -0
- package/src/claude/scripts/spec-ground.cjs +571 -173
- package/src/claude/scripts/spec-readiness.cjs +425 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +67 -16
- package/src/claude/scripts/spec-scaffold.cjs +1594 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3110 -218
- package/src/claude/scripts/workflow-policy.cjs +501 -183
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/code-review/SKILL.md +12 -3
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +47 -60
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +19 -12
- package/src/claude/skills/specs/SKILL.md +183 -177
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +214 -262
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +35 -85
- package/src/claude/skills/specs/templates/task.md +30 -102
- package/src/claude/skills/sync/SKILL.md +33 -10
- package/src/claude/skills/sync/references/sync-protocols.md +31 -13
- package/src/claude/skills/test/SKILL.md +17 -7
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +43 -240
- package/src/codex/hooks/completion-authority-state.cjs +10 -8
- package/src/codex/hooks/completion-authority.cjs +13 -7
- package/src/codex/hooks/lib/hook-context.cjs +16 -2
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +61 -38
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
- package/src/codex/hooks/lib/spec-utils.cjs +56 -5
- package/src/codex/hooks/privacy-approval.cjs +6 -2
- package/src/codex/hooks/privacy-block.cjs +94 -15
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/spec-gate.cjs +49 -34
- package/src/codex/hooks/spec-state.cjs +10 -3
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +6 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
|
@@ -11,21 +11,49 @@ const ROOT = path.resolve(__dirname, '../..');
|
|
|
11
11
|
const VALIDATOR = path.join(ROOT, 'src/claude/scripts/validate-spec-output.cjs');
|
|
12
12
|
const GROUNDING = path.join(ROOT, 'src/claude/scripts/spec-ground.cjs');
|
|
13
13
|
const SCAFFOLD = path.join(ROOT, 'src/claude/scripts/spec-scaffold.cjs');
|
|
14
|
+
const AUTHORING_VALIDATION = path.join(ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
|
|
14
15
|
const RESOLVER = path.join(ROOT, 'src/claude/scripts/spec-resolver.cjs');
|
|
15
16
|
const POLICY = require(path.join(ROOT, 'src/claude/scripts/workflow-policy.cjs'));
|
|
16
17
|
const SPEC_RESOLVER = require(RESOLVER);
|
|
18
|
+
const { normalizeCodexBody } = require(path.join(ROOT, 'bin/lib/codex-install.js'));
|
|
19
|
+
const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
|
|
17
20
|
|
|
18
21
|
function write(filePath, content) {
|
|
19
22
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
20
23
|
fs.writeFileSync(filePath, content);
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
function writeRaw(filePath, content) {
|
|
27
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
28
|
+
fs.writeFileSync(filePath, content);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function copyCodexRuntime(adapterRoot, entrypoints) {
|
|
32
|
+
const files = copyClaudeTestRuntime(ROOT, adapterRoot, entrypoints);
|
|
33
|
+
for (const relative of files) {
|
|
34
|
+
if (!/\.(?:cjs|js)$/.test(relative)) continue;
|
|
35
|
+
const target = path.join(adapterRoot, relative);
|
|
36
|
+
writeRaw(
|
|
37
|
+
target,
|
|
38
|
+
normalizeCodexBody(fs.readFileSync(target, 'utf8'), `src/claude/${relative}`),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
function run(script, args, cwd, options = {}) {
|
|
45
|
+
const fixtureRoot = script === VALIDATOR && typeof args[0] === 'string'
|
|
46
|
+
? path.dirname(path.dirname(path.resolve(args[0])))
|
|
47
|
+
: null;
|
|
24
48
|
return spawnSync(process.execPath, [script, ...args], {
|
|
25
49
|
cwd,
|
|
26
50
|
encoding: 'utf8',
|
|
27
51
|
...options,
|
|
28
|
-
env:
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
...(fixtureRoot ? { HOME: path.join(fixtureRoot, '.home'), USERPROFILE: path.join(fixtureRoot, '.home') } : {}),
|
|
55
|
+
...(options.env || {}),
|
|
56
|
+
},
|
|
29
57
|
});
|
|
30
58
|
}
|
|
31
59
|
|
|
@@ -49,33 +77,208 @@ function snapshotTree(root) {
|
|
|
49
77
|
return entries.sort().join('\n');
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
function task({
|
|
53
|
-
|
|
80
|
+
function task({
|
|
81
|
+
id = 'R1-01',
|
|
82
|
+
mapping = '1.1',
|
|
83
|
+
related = '`src/one.js` | Modify',
|
|
84
|
+
relatedRows = null,
|
|
85
|
+
dependencies = 'none',
|
|
86
|
+
status = 'pending',
|
|
87
|
+
parallel = false,
|
|
88
|
+
extra = '',
|
|
89
|
+
} = {}) {
|
|
90
|
+
const relatedPath = related.match(/`([^`]+)`/)?.[1] || 'src/one.js';
|
|
91
|
+
const action = related.match(/\|\s*([A-Za-z]+)\b/i)?.[1] || 'Modify';
|
|
92
|
+
const acceptanceId = mapping.match(/\d+\.\d+/)?.[0];
|
|
93
|
+
const lifecycleRows = relatedRows === null ? (action.toLowerCase() === 'modify'
|
|
94
|
+
? `| \`${relatedPath}\` | Read | Inspect existing implementation |\n| ${related} | Update implementation |`
|
|
95
|
+
: `| ${related} | Apply declared lifecycle action |`) : relatedRows;
|
|
96
|
+
return `# Task ${id}: One\n\n**Status:** ${status}\n\n## Outcome\n\nCalling the real fixture entrypoint returns status value 1.\n\n## Scope and Typed Anchors\n\n- **In scope:** Implement the mapped observable behavior.\n- **Out of scope:** Unrelated runtime behavior.\n- **Contracts/Invariants:** none\n- **Canonical design anchors consumed:** A-D-01\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n| A-${id}-01 | command | \`node --test test/one.test.js\` | planned verification command |\n\n## Changes\n\n- [ ] ${parallel ? '(P) ' : ''}Update the source boundary and preserve its negative path.${mapping ? ` _Requirements: ${mapping}_` : ''}\n\n## Acceptance\n\n${acceptanceId ? `- **R${acceptanceId}:**` : '-'} Calling \`${relatedPath}\` returns an observable result with status value 1.\n\n## Dependencies\n\n- ${dependencies}\n\n## Verification Plan\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and one assertion for status value 1\n- **Negative path:** invalid input returns a deterministic error result\n- **Reachability:** \`${relatedPath}\` is invoked by the focused test\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|---|\n${lifecycleRows}\n${extra}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function legacyWorkflowPolicy(lane = 'Standard') {
|
|
100
|
+
const proofObligations = lane === 'Critical'
|
|
101
|
+
? ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding']
|
|
102
|
+
: ['needsInspection', 'needsExecutionProof'];
|
|
103
|
+
return {
|
|
104
|
+
version: '1',
|
|
105
|
+
lane,
|
|
106
|
+
automatic_lane: lane,
|
|
107
|
+
risks: lane === 'Critical' ? ['auth'] : [],
|
|
108
|
+
artifact_profile: lane === 'Critical' ? 'strict' : 'bounded',
|
|
109
|
+
proof_obligations: proofObligations,
|
|
110
|
+
actor_needs: proofObligations.map((obligation) => ({
|
|
111
|
+
capability: {
|
|
112
|
+
needsInspection: 'inspection',
|
|
113
|
+
needsExecutionProof: 'execution-proof',
|
|
114
|
+
needsIndependentAudit: 'audit',
|
|
115
|
+
needsResearchGrounding: 'research-grounding',
|
|
116
|
+
}[obligation],
|
|
117
|
+
independence: obligation === 'needsIndependentAudit' ? 'independent' : 'same-session',
|
|
118
|
+
})),
|
|
119
|
+
override_receipt: null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function legacyTask({ relatedRows = '| `src/one.js` | Read | Inspect existing behavior |', extra = '' } = {}) {
|
|
124
|
+
return `# Task R1-01: One\n\n**Status:** pending\n**Dependencies:** none\n\n## Context\n\nThe existing fixture service returns one deterministic result.\n\n## Constraints\n\nPreserve the stable result contract and its invalid-input behavior.\n\n## Steps\n\n- [ ] Inspect the declared boundary and preserve status value 1. _Requirements: 1.1_\n\n## Requirements\n\n- 1.1 — Return status value 1 from the fixture service.\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|\n${relatedRows}\n\n## Completion Criteria\n\n- Calling \`src/one.js\` returns result status value 1.\n\n## Evidence\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and status value 1\n- **Inspect:** \`src/one.js\` runtime result\n- Runtime reachability verification\n - **Entrypoint/caller:** \`src/one.js\`\n\n## Risk Assessment\n\n| Risk | Severity | Mitigation |\n|---|---|---|\n| regression | low | focused test |\n${extra}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function lifecycleTimestamps({ tasks = true, research = false, review = false } = {}) {
|
|
128
|
+
return {
|
|
129
|
+
init: '2026-08-11T00:00:00+07:00',
|
|
130
|
+
requirements_done: '2026-08-11T00:01:00+07:00',
|
|
131
|
+
research_done: research ? '2026-08-11T00:02:00+07:00' : null,
|
|
132
|
+
design_done: '2026-08-11T00:03:00+07:00',
|
|
133
|
+
tasks_done: tasks ? '2026-08-11T00:04:00+07:00' : null,
|
|
134
|
+
code_done: null,
|
|
135
|
+
test_done: null,
|
|
136
|
+
review_done: review ? '2026-08-11T00:05:00+07:00' : null,
|
|
137
|
+
validation_done: '2026-08-11T00:06:00+07:00',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function semanticReviewNotRun() {
|
|
142
|
+
return {
|
|
143
|
+
status: 'not-run', reviewed_artifact_digest: null, reviewed_criteria: [], counterexamples: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function semanticDigest(specDir) {
|
|
148
|
+
const result = run(VALIDATOR, [specDir, '--semantic-digest'], ROOT);
|
|
149
|
+
assert.equal(result.status, 0, output(result));
|
|
150
|
+
return result.stdout.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function completeSemanticReview(specDir, independence = 'same-session') {
|
|
154
|
+
const statePath = path.join(specDir, 'spec.json');
|
|
155
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
156
|
+
state.validation = isPlainObjectForTest(state.validation) ? state.validation : {};
|
|
157
|
+
state.validation.semantic_review = {
|
|
158
|
+
status: 'completed',
|
|
159
|
+
reviewed_artifact_digest: semanticDigest(specDir),
|
|
160
|
+
reviewed_criteria: ['R1.1'],
|
|
161
|
+
counterexamples: [{
|
|
162
|
+
criterion: 'R1.1',
|
|
163
|
+
scenario: 'The fixture receives invalid input at its runtime boundary.',
|
|
164
|
+
expected: 'The fixture returns the deterministic negative-path result.',
|
|
165
|
+
design_reference: 'D1',
|
|
166
|
+
}],
|
|
167
|
+
};
|
|
168
|
+
write(statePath, JSON.stringify(state, null, 2));
|
|
169
|
+
if (independence === 'independent') {
|
|
170
|
+
const root = path.dirname(path.dirname(specDir));
|
|
171
|
+
const featureName = state.feature_name;
|
|
172
|
+
const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({
|
|
173
|
+
feature_name: featureName,
|
|
174
|
+
spec_file: path.relative(root, statePath),
|
|
175
|
+
semantic_digest: state.validation.semantic_review.reviewed_artifact_digest,
|
|
176
|
+
verdict: 'PASS',
|
|
177
|
+
})}`;
|
|
178
|
+
const result = spawnSync(process.execPath, [path.join(ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
|
|
179
|
+
cwd: root,
|
|
180
|
+
env: { ...process.env, HOME: path.join(root, '.home'), USERPROFILE: path.join(root, '.home'), PROJECT_ROOT: root },
|
|
181
|
+
input: JSON.stringify({ hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'review-agent', agent_type: 'code_auditor', last_assistant_message: claim, cwd: root }),
|
|
182
|
+
encoding: 'utf8',
|
|
183
|
+
});
|
|
184
|
+
assert.equal(result.status, 0, output(result));
|
|
185
|
+
assert.equal(result.stderr, '');
|
|
186
|
+
}
|
|
187
|
+
return state;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isPlainObjectForTest(value) {
|
|
191
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function obligationClosureBlock(obligation) {
|
|
195
|
+
const consumers = obligation.consumer_tasks.length > 0
|
|
196
|
+
? obligation.consumer_tasks.join(', ')
|
|
197
|
+
: 'none';
|
|
198
|
+
return `<!-- implementation-obligation:${obligation.id} -->\n- Requirements: ${obligation.requirements.join(', ')}\n- Canonical state: ${obligation.canonical_state}\n- Authority: ${obligation.authority}\n- Recovery: ${obligation.recovery}\n- Owner task: ${obligation.owner_task}\n- Consumers: ${consumers}\n- Verification: ${obligation.verification}\n- Evidence: ${obligation.evidence}\n`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function executionClosure(obligation) {
|
|
202
|
+
return `\n## Execution Closure\n\n${obligationClosureBlock(obligation)}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function executionClosureTable(obligation) {
|
|
206
|
+
const consumers = obligation.consumer_tasks.length > 0
|
|
207
|
+
? obligation.consumer_tasks.join(', ')
|
|
208
|
+
: 'none';
|
|
209
|
+
return `\n## Execution Closure\n\n<!-- implementation-obligation:${obligation.id} -->\n` +
|
|
210
|
+
'| Obligation ID / Requirements | Canonical State | Authority | Crash / Retry Recovery | Owner Task | Consumer Tasks / Dependency Order | Verification / Evidence |\n' +
|
|
211
|
+
'|---|---|---|---|---|---|---|\n' +
|
|
212
|
+
`| ${obligation.id} / ${obligation.requirements.join(', ')} | ${obligation.canonical_state} | ${obligation.authority} | ${obligation.recovery} | \`${obligation.owner_task}\` | ${consumers} | \`${obligation.verification}\` → ${obligation.evidence} |\n`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function criticalNoObligationDesign() {
|
|
216
|
+
return designFixture() +
|
|
217
|
+
'## Execution Closure\n\n' +
|
|
218
|
+
'N/A — this fixture has no cross-task state, retry, migration, or authority obligation.\n';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function designFixture() {
|
|
222
|
+
return '# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n' +
|
|
223
|
+
'## Typed Anchors\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n' +
|
|
224
|
+
'| A-D-01 | file | `src/one.js` | existing owner and runtime entrypoint |\n\n' +
|
|
225
|
+
'## Decisions and Invariants\n\n### D1 — Stable result\n\n- **Decision:** Return status value 1.\n- **Negative path:** Invalid input returns a deterministic error.\n- **Anchors:** A-D-01\n\n' +
|
|
226
|
+
'## Verification\n\n| Requirement | Proof target | Expected result | Negative path / reachability |\n|---|---|---|---|\n' +
|
|
227
|
+
'| R1.1 | `node --test test/one.test.js` | exit code 0 and status value 1 | invalid input through `src/one.js` |\n\n';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function designFixture21() {
|
|
231
|
+
return '# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n' +
|
|
232
|
+
'## Typed Anchors\n\n| ID | Type | Target | Role | Access | Action |\n|---|---|---|---|---|---|\n' +
|
|
233
|
+
'| A-D-01 | file | `src/one.js` | runtime entrypoint | read | read |\n' +
|
|
234
|
+
'| A-D-02 | command | `node --test test/one.test.js` | proof command | read | read |\n\n' +
|
|
235
|
+
'## Decisions and Invariants\n\n### D1 — Stable result\n\nReturn status value 1 and reject invalid input.\n\n' +
|
|
236
|
+
'### I1 — Invalid-state invariant\n\nInvalid input never reports the success status.\n\n' +
|
|
237
|
+
'### C1 — Result contract\n\nThe result contains one numeric status.\n\n' +
|
|
238
|
+
'## Verification Definitions\n\n' +
|
|
239
|
+
'- **V1**: Subject criteria R1.1; Subject owner A-D-01; Proof criteria R1.2; Proof owner A-D-02; Evidence anchor A-D-02; Decision refs D1, I1, C1; Method command `node --test test/one.test.js`; Expected exit code 0 and status value 1; Negative/failure invalid input returns a deterministic error; Reachability/grounding entrypoint `src/one.js` via A-D-01, A-D-02.\n';
|
|
54
240
|
}
|
|
55
241
|
|
|
56
242
|
function createSpec(root, options = {}) {
|
|
57
|
-
const
|
|
243
|
+
const featureName = options.name || 'spec';
|
|
244
|
+
const specDir = path.join(root, 'specs', featureName);
|
|
58
245
|
const taskPath = 'tasks/task-R1-01-one.md';
|
|
59
|
-
write(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
246
|
+
write(
|
|
247
|
+
path.join(specDir, 'requirements.md'),
|
|
248
|
+
options.requirements || '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** When input arrives, the service shall return one deterministic result.\n',
|
|
249
|
+
);
|
|
250
|
+
write(
|
|
251
|
+
path.join(specDir, 'design.md'),
|
|
252
|
+
options.design || designFixture(),
|
|
253
|
+
);
|
|
63
254
|
write(path.join(specDir, taskPath), options.task || task());
|
|
255
|
+
write(path.join(root, 'test/one.test.js'), "'use strict';\n// Separate proof artifact for fixture validation.\n");
|
|
64
256
|
write(path.join(specDir, 'spec.json'), JSON.stringify({
|
|
65
257
|
schema_version: '2.0',
|
|
66
|
-
feature_name:
|
|
258
|
+
feature_name: featureName,
|
|
259
|
+
created_at: '2026-08-11T00:00:00+07:00',
|
|
260
|
+
updated_at: '2026-08-11T00:05:00+07:00',
|
|
261
|
+
language: 'en',
|
|
67
262
|
status: 'in_progress',
|
|
68
263
|
current_phase: 'tasks',
|
|
69
264
|
scope_lock: { source: 'fixture', in_scope: ['1'], out_of_scope: [], expansion_policy: 'requires-user-approval' },
|
|
265
|
+
coordination: {
|
|
266
|
+
tasks_required: true,
|
|
267
|
+
phases_required: false,
|
|
268
|
+
reason: 'task_topology',
|
|
269
|
+
task_triggers: ['separate_proof'],
|
|
270
|
+
},
|
|
70
271
|
task_files: [taskPath],
|
|
71
272
|
task_registry: {
|
|
72
273
|
[taskPath]: {
|
|
73
274
|
id: 'R1-01', title: 'One', status: 'pending', dependencies: [], blocker: null,
|
|
74
275
|
started_at: null, completed_at: null, last_updated_at: null,
|
|
276
|
+
artifacts: ['test/one.test.js'],
|
|
75
277
|
},
|
|
76
278
|
},
|
|
77
279
|
approvals: { requirements: { generated: true, agent_validated: true, user_approved: true }, design: { generated: true, agent_validated: true, user_approved: true }, tasks: { generated: true, agent_validated: true, user_approved: true } },
|
|
78
|
-
validation: { status: 'not-run' },
|
|
280
|
+
validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
|
|
281
|
+
timestamps: { ...lifecycleTimestamps(), validation_done: null },
|
|
79
282
|
ready_for_implementation: false,
|
|
80
283
|
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
|
|
81
284
|
...(options.spec || {}),
|
|
@@ -86,29 +289,67 @@ function createSpec(root, options = {}) {
|
|
|
86
289
|
function createBoundedSpec(root, name = 'bounded') {
|
|
87
290
|
const specDir = path.join(root, name);
|
|
88
291
|
const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
|
|
89
|
-
write(path.join(specDir, 'requirements.md'), '# Requirements\n\
|
|
90
|
-
write(path.join(specDir, 'design.md'),
|
|
91
|
-
write(path.join(specDir, 'feature-receipt.md'), '# Feature Verification Receipt\n\nVerification: PENDING\n\nStatus: in_progress\n\nBlocker: awaiting execution proof\n');
|
|
292
|
+
write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** When input arrives, the service shall return status value 1.\n');
|
|
293
|
+
write(path.join(specDir, 'design.md'), designFixture());
|
|
92
294
|
write(path.join(specDir, 'spec.json'), JSON.stringify({
|
|
93
295
|
schema_version: '2.0',
|
|
94
296
|
feature_name: name,
|
|
297
|
+
created_at: '2026-08-11T00:00:00+07:00',
|
|
298
|
+
updated_at: '2026-08-11T00:03:00+07:00',
|
|
95
299
|
status: 'in_progress',
|
|
96
300
|
current_phase: 'design',
|
|
97
301
|
scope_lock: { source: 'fixture', in_scope: [], out_of_scope: [], expansion_policy: 'requires-user-approval' },
|
|
302
|
+
coordination: { tasks_required: false, phases_required: false, reason: 'taskless_requested' },
|
|
98
303
|
approvals: {
|
|
99
304
|
requirements: { generated: true, agent_validated: true, user_approved: false },
|
|
100
305
|
design: { generated: true, agent_validated: true, user_approved: false },
|
|
101
306
|
},
|
|
102
|
-
validation: { status: 'not-run' },
|
|
307
|
+
validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
|
|
308
|
+
timestamps: { ...lifecycleTimestamps({ tasks: false }), validation_done: null },
|
|
103
309
|
ready_for_implementation: false,
|
|
104
310
|
workflow_policy: policy,
|
|
105
311
|
}, null, 2));
|
|
106
312
|
return specDir;
|
|
107
313
|
}
|
|
108
314
|
|
|
109
|
-
function
|
|
315
|
+
function legacyTasklessSpec20State(name, policy) {
|
|
316
|
+
return {
|
|
317
|
+
schema_version: '2.0',
|
|
318
|
+
feature_name: name,
|
|
319
|
+
created_at: '2026-08-11T00:00:00+07:00',
|
|
320
|
+
updated_at: '2026-08-11T00:03:00+07:00',
|
|
321
|
+
language: 'en',
|
|
322
|
+
status: 'in_progress',
|
|
323
|
+
current_phase: 'design',
|
|
324
|
+
scope_lock: {
|
|
325
|
+
source: 'legacy fixture',
|
|
326
|
+
in_scope: [],
|
|
327
|
+
out_of_scope: [],
|
|
328
|
+
expansion_policy: 'requires-user-approval',
|
|
329
|
+
},
|
|
330
|
+
coordination: {
|
|
331
|
+
tasks_required: false,
|
|
332
|
+
phases_required: false,
|
|
333
|
+
reason: 'taskless_requested',
|
|
334
|
+
},
|
|
335
|
+
validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
|
|
336
|
+
timestamps: { ...lifecycleTimestamps({ tasks: false }), validation_done: null },
|
|
337
|
+
ready_for_implementation: false,
|
|
338
|
+
workflow_policy: policy,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function createLegacyTasklessSpec20(root, name, policy) {
|
|
110
343
|
const specDir = path.join(root, 'specs', name);
|
|
111
|
-
write(
|
|
344
|
+
write(
|
|
345
|
+
path.join(specDir, 'requirements.md'),
|
|
346
|
+
'# Requirements\n\n### Requirement 1: Legacy baseline\n\n- **R1.1** The fixture shall preserve one deterministic result.\n',
|
|
347
|
+
);
|
|
348
|
+
write(path.join(specDir, 'design.md'), designFixture());
|
|
349
|
+
write(
|
|
350
|
+
path.join(specDir, 'spec.json'),
|
|
351
|
+
JSON.stringify(legacyTasklessSpec20State(name, policy), null, 2),
|
|
352
|
+
);
|
|
112
353
|
return specDir;
|
|
113
354
|
}
|
|
114
355
|
|
|
@@ -117,19 +358,236 @@ function output(result) {
|
|
|
117
358
|
}
|
|
118
359
|
|
|
119
360
|
function completeScaffoldedSpec(specDir) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
361
|
+
const state = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
362
|
+
for (const [index, taskFile] of (state.task_files || []).entries()) {
|
|
363
|
+
const entry = state.task_registry[taskFile];
|
|
364
|
+
write(path.join(specDir, taskFile), task({
|
|
365
|
+
id: entry.id,
|
|
366
|
+
related: `\`src/${index === 0 ? 'one' : `task-${index + 1}`}.js\` | Read`,
|
|
367
|
+
dependencies: entry.dependencies.length > 0 ? entry.dependencies.join(',') : 'none',
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
127
370
|
write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: Artifact\n\n- **R1.1** The task is verifiable.\n');
|
|
128
|
-
write(
|
|
129
|
-
|
|
130
|
-
|
|
371
|
+
write(
|
|
372
|
+
path.join(specDir, 'design.md'),
|
|
373
|
+
state.workflow_policy?.assurance_level === 'Strict' ? criticalNoObligationDesign() : designFixture(),
|
|
374
|
+
);
|
|
375
|
+
if (state.research === 'research.md') {
|
|
376
|
+
write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n- Producer fixture.\n');
|
|
377
|
+
}
|
|
378
|
+
return state;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
test('canonical legacy v2.0 taskless migration fixture is valid without execution evidence', () => {
|
|
382
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-legacy-taskless-baseline-'));
|
|
383
|
+
try {
|
|
384
|
+
const specDir = createLegacyTasklessSpec20(
|
|
385
|
+
root,
|
|
386
|
+
'legacy-taskless-baseline',
|
|
387
|
+
POLICY.workflowPolicySnapshot({ riskSignals: {} }),
|
|
388
|
+
);
|
|
389
|
+
const state = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
390
|
+
assert.equal(state.schema_version, '2.0');
|
|
391
|
+
assert.equal(state.ready_for_implementation, false);
|
|
392
|
+
assert.equal(Object.hasOwn(state, 'task_files'), false);
|
|
393
|
+
assert.equal(Object.hasOwn(state, 'task_registry'), false);
|
|
394
|
+
assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
|
|
395
|
+
assert.equal(fs.existsSync(path.join(specDir, 'receipts')), false);
|
|
396
|
+
state.override_receipt = { legacy: true };
|
|
397
|
+
write(path.join(specDir, 'spec.json'), JSON.stringify(state, null, 2));
|
|
398
|
+
const validation = run(VALIDATOR, [specDir], ROOT);
|
|
399
|
+
assert.equal(validation.status, 0, output(validation));
|
|
400
|
+
} finally {
|
|
401
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
function completeScaffoldedSpec21(specDir) {
|
|
406
|
+
const statePath = path.join(specDir, 'spec.json');
|
|
407
|
+
let state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
408
|
+
const fixtureRoot = path.dirname(path.dirname(specDir));
|
|
409
|
+
const criteria = (state.task_files || []).map((_, index) => `R1.${index + 1}`);
|
|
410
|
+
write(path.join(fixtureRoot, 'test/one.test.js'), "require('node:test')('one', () => {});\n");
|
|
411
|
+
fs.mkdirSync(path.join(fixtureRoot, 'artifacts'), { recursive: true });
|
|
412
|
+
for (const [index, taskFile] of (state.task_files || []).entries()) {
|
|
413
|
+
const entry = state.task_registry[taskFile];
|
|
414
|
+
const ownedPath = `src/task-${index + 1}.js`;
|
|
415
|
+
const criterion = criteria[index];
|
|
416
|
+
const taskRole = index === 0 ? 'subject' : 'verifier';
|
|
417
|
+
write(path.join(fixtureRoot, ownedPath), 'module.exports = { status: 1 };\n');
|
|
418
|
+
write(path.join(specDir, taskFile), `# Task ${entry.id}: ${entry.title}
|
|
419
|
+
**Status:** pending
|
|
420
|
+
|
|
421
|
+
## Outcome
|
|
422
|
+
Return observable status value 1 through the real fixture entrypoint.
|
|
423
|
+
|
|
424
|
+
## Scope
|
|
425
|
+
- **In scope:** Exact behavior for ${ownedPath}.
|
|
426
|
+
- **Out of scope:** Unrelated runtime behavior.
|
|
427
|
+
|
|
428
|
+
## Anchors and Ownership
|
|
429
|
+
| ID | Type | Target | Role | Access | Action |
|
|
430
|
+
|---|---|---|---|---|---|
|
|
431
|
+
| A-${entry.id}-01 | file | \`${ownedPath}\` | owner | write | modify |
|
|
432
|
+
${taskRole === 'verifier' ? `| A-${entry.id}-02 | artifact | \`artifacts/${entry.id}.json\` | verifier | write | create |` : ''}
|
|
433
|
+
|
|
434
|
+
## Changes
|
|
435
|
+
- [ ] ${taskRole === 'verifier' ? 'Implement the separate proof behavior.' : 'Implement the exact owned behavior.'} _Requirements: ${criterion.slice(1)}_
|
|
436
|
+
|
|
437
|
+
## Acceptance
|
|
438
|
+
- **${criterion}:** ${taskRole === 'verifier' ? 'The focused proof reports status value 1 and rejects an invalid result.' : 'The fixture returns status value 1 and rejects invalid input.'}
|
|
439
|
+
|
|
440
|
+
## Dependencies
|
|
441
|
+
- ${entry.dependencies.length > 0 ? entry.dependencies.join(', ') : 'none'}
|
|
442
|
+
|
|
443
|
+
## Verification Plan
|
|
444
|
+
- **Verification ref:** V1
|
|
445
|
+
- **Task role:** ${taskRole}
|
|
446
|
+
- **Command:** \`node --test test/one.test.js\`
|
|
447
|
+
- **Expected:** Exit code 0 and exact status value 1.
|
|
448
|
+
- **Negative path:** Invalid input returns rejected state.
|
|
449
|
+
- **Reachability:** \`${ownedPath}\`
|
|
450
|
+
`);
|
|
451
|
+
}
|
|
452
|
+
write(path.join(specDir, 'requirements.md'), `# Requirements
|
|
453
|
+
|
|
454
|
+
### Requirement 1: Artifact
|
|
455
|
+
|
|
456
|
+
${criteria.map((criterion, index) => `- **${criterion}**: Task ${index + 1} shall expose its distinct observable status or proof outcome and reject invalid input.`).join('\n')}
|
|
457
|
+
`);
|
|
458
|
+
write(path.join(specDir, 'design.md'), `# Design
|
|
459
|
+
|
|
460
|
+
## Architecture
|
|
461
|
+
The fixture entrypoint delegates to the owned task boundary.
|
|
462
|
+
|
|
463
|
+
## Typed Anchors
|
|
464
|
+
| ID | Type | Target | Role | Access | Action |
|
|
465
|
+
|---|---|---|---|---|---|
|
|
466
|
+
| A-D-01 | command | \`node --test test/one.test.js\` | verification command | read | read |
|
|
467
|
+
|
|
468
|
+
## Canonical Contracts & Invariants
|
|
469
|
+
### D1 — Delegation decision
|
|
470
|
+
The entrypoint delegates exactly once.
|
|
471
|
+
### I1 — Invalid-state invariant
|
|
472
|
+
Invalid input never returns status value 1.
|
|
473
|
+
### C1 — Result contract
|
|
474
|
+
The result contains a numeric status.
|
|
475
|
+
### T1 — Artifact transition
|
|
476
|
+
The transition records success, failure, and recovery states.
|
|
477
|
+
|
|
478
|
+
## Verification Definitions
|
|
479
|
+
- **V1**: Subject criteria ${criteria[0]}; Subject owner ${state.task_registry[state.task_files[0]].id}; Proof criteria ${criteria.slice(1).join(', ')}; Proof owner ${state.task_registry[state.task_files[1]].id}; Evidence anchor A-${state.task_registry[state.task_files[1]].id}-02; Decision refs D1, I1, C1; Method command \`node --test test/one.test.js\`; Expected exit code 0 and exact status value 1; Negative/failure invalid input returns rejected state; Reachability/grounding entrypoint \`src/task-1.js\` via A-D-01, A-${state.task_registry[state.task_files[1]].id}-02.
|
|
480
|
+
`);
|
|
481
|
+
const promoted = run(
|
|
482
|
+
SCAFFOLD,
|
|
483
|
+
[path.basename(specDir), '--sync-semantic-model'],
|
|
484
|
+
path.dirname(path.dirname(specDir)),
|
|
485
|
+
);
|
|
486
|
+
assert.equal(promoted.status, 0, output(promoted));
|
|
487
|
+
// I21: `validated` is never hand-authored here. The real C16 coordinator is
|
|
488
|
+
// the only writer, and it flips the enums only after its own clean
|
|
489
|
+
// validator+grounder pass, writing the matching digest receipt in the same
|
|
490
|
+
// atomic replace. Routing the fixture through it keeps this helper honest and
|
|
491
|
+
// exercises the production path end to end.
|
|
492
|
+
const authored = run(AUTHORING_VALIDATION, [specDir, '--root', fixtureRoot], fixtureRoot);
|
|
493
|
+
assert.equal(authored.status, 0, output(authored));
|
|
494
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
495
|
+
assert.equal(state.authoring.requirements, 'validated');
|
|
496
|
+
assert.equal(state.authoring.design, 'validated');
|
|
497
|
+
state.validation.status = 'completed';
|
|
498
|
+
state.validation.semantic_review = {
|
|
499
|
+
status: 'completed', semantic_digest: null, verdict: 'PASS', lifecycle_disposition: 'CONTINUE',
|
|
500
|
+
findings: [], unresolved_decisions: [],
|
|
501
|
+
graph_coverage: [
|
|
502
|
+
'criterion_local', 'cross_criterion', 'runtime_path',
|
|
503
|
+
'assumption_provenance', 'compatibility_migration',
|
|
504
|
+
].map((surface) => ({ surface, covered: true, notes: `${surface} reviewed against the fixture topology.` })),
|
|
505
|
+
repair_round: 0,
|
|
506
|
+
reviewed_criteria: criteria,
|
|
507
|
+
counterexamples: criteria.map((criterion, index) => ({
|
|
508
|
+
criterion, case_kind: index % 2 === 0 ? 'failure' : 'adversarial',
|
|
509
|
+
scenario: `Task ${index + 1} receives a distinct malformed status payload at its declared boundary.`,
|
|
510
|
+
expected: `Task ${index + 1} rejects that payload without publishing its distinct success outcome.`,
|
|
511
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
512
|
+
})),
|
|
513
|
+
reviewer_evidence: {
|
|
514
|
+
assurance: 'Routine',
|
|
515
|
+
summary: 'Fixture reviewer confirmed every criterion, counterexample, and reachability anchor.',
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
write(statePath, JSON.stringify(state, null, 2));
|
|
519
|
+
state.validation.semantic_review.semantic_digest = semanticDigest(specDir);
|
|
520
|
+
state.ready_for_implementation = true;
|
|
521
|
+
write(statePath, JSON.stringify(state, null, 2));
|
|
522
|
+
return state;
|
|
131
523
|
}
|
|
132
524
|
|
|
525
|
+
test('Codex installer transform preserves all owned spec assets with idempotent runtime parity', () => {
|
|
526
|
+
for (const relative of [
|
|
527
|
+
'src/claude/skills/specs/SKILL.md',
|
|
528
|
+
'src/claude/skills/specs/rules/tasks-generation.md',
|
|
529
|
+
'src/claude/skills/specs/rules/design-principles.md',
|
|
530
|
+
'src/claude/skills/specs/references/review.md',
|
|
531
|
+
'src/claude/skills/specs/templates/design.md',
|
|
532
|
+
'src/claude/skills/specs/templates/task.md',
|
|
533
|
+
'src/claude/scripts/spec-scaffold.cjs',
|
|
534
|
+
'src/claude/scripts/spec-receipt.cjs',
|
|
535
|
+
'src/claude/scripts/validate-spec-output.cjs',
|
|
536
|
+
]) {
|
|
537
|
+
const source = fs.readFileSync(path.join(ROOT, relative), 'utf8');
|
|
538
|
+
const transformed = normalizeCodexBody(source, relative);
|
|
539
|
+
assert.equal(normalizeCodexBody(transformed, relative), transformed, `${relative} must normalize idempotently`);
|
|
540
|
+
assert.doesNotMatch(transformed, /node \.claude\/scripts\//, `${relative} retained a Claude executable path`);
|
|
541
|
+
if (/node \.claude\/scripts\//.test(source)) {
|
|
542
|
+
assert.match(transformed, /node \.codex\/scripts\//, `${relative} lost its Codex executable path`);
|
|
543
|
+
}
|
|
544
|
+
if (/implementation-obligation:/.test(source)) {
|
|
545
|
+
assert.match(transformed, /implementation-obligation:/, `${relative} lost closure semantics`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const skill = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/SKILL.md'), 'utf8');
|
|
550
|
+
const review = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/references/review.md'), 'utf8');
|
|
551
|
+
const designRules = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/rules/design-principles.md'), 'utf8');
|
|
552
|
+
const taskRules = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/rules/tasks-generation.md'), 'utf8');
|
|
553
|
+
assert.match(skill, /\| `Compact` \| `spec\.json`, `requirements\.md`, `design\.md` \| only on a research trigger \| only on a typed topology trigger \|/);
|
|
554
|
+
assert.match(skill, /\| `Full` \| `spec\.json`, `requirements\.md`, `design\.md` \| only on a research trigger \| only on a typed topology trigger \|/);
|
|
555
|
+
assert.doesNotMatch(review, /Standard[^\n]{0,100}(?:must not|forbidden|never)[^\n]{0,60}tasks?/i);
|
|
556
|
+
assert.match(designRules, /## Conditional detail[\s\S]*Migration:[\s\S]*only when existing state or public contracts change/);
|
|
557
|
+
assert.match(taskRules, /distinct ownership, a durable[\s\S]{0,180}parallel coordination/);
|
|
558
|
+
assert.match(taskRules, /receipts\/<task-basename>\.md|execution closeout/);
|
|
559
|
+
|
|
560
|
+
const parityRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-validator-parity-'));
|
|
561
|
+
try {
|
|
562
|
+
const installedScripts = path.join(parityRoot, '.codex/scripts');
|
|
563
|
+
const installedValidator = path.join(installedScripts, 'validate-spec-output.cjs');
|
|
564
|
+
copyCodexRuntime(path.dirname(installedScripts), ['scripts/validate-spec-output.cjs']);
|
|
565
|
+
|
|
566
|
+
const scaffolded = run(SCAFFOLD, ['runtime-parity'], parityRoot);
|
|
567
|
+
assert.equal(scaffolded.status, 0, output(scaffolded));
|
|
568
|
+
const specDir = path.join(parityRoot, 'specs/runtime-parity');
|
|
569
|
+
write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** The runtime shall return status value 1.\n- **R1.2** The proof shall reject an invalid status result.\n');
|
|
570
|
+
write(path.join(specDir, 'design.md'), designFixture21());
|
|
571
|
+
const promoted = run(SCAFFOLD, ['runtime-parity', '--sync-semantic-model'], parityRoot);
|
|
572
|
+
assert.equal(promoted.status, 0, output(promoted));
|
|
573
|
+
const sourcePending = run(VALIDATOR, [specDir], ROOT);
|
|
574
|
+
const codexPending = run(installedValidator, [specDir], ROOT);
|
|
575
|
+
assert.equal(sourcePending.status, 0, output(sourcePending));
|
|
576
|
+
assert.equal(codexPending.status, sourcePending.status, output(codexPending));
|
|
577
|
+
|
|
578
|
+
const statePath = path.join(specDir, 'spec.json');
|
|
579
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
580
|
+
state.status = 'paused';
|
|
581
|
+
writeRaw(statePath, JSON.stringify(state, null, 2));
|
|
582
|
+
const sourceDonePending = run(VALIDATOR, [specDir], ROOT);
|
|
583
|
+
const codexDonePending = run(installedValidator, [specDir], ROOT);
|
|
584
|
+
assert.equal(sourceDonePending.status, 0, output(sourceDonePending));
|
|
585
|
+
assert.equal(codexDonePending.status, sourceDonePending.status, output(codexDonePending));
|
|
586
|
+
} finally {
|
|
587
|
+
fs.rmSync(parityRoot, { recursive: true, force: true });
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
133
591
|
test('resolver binds feature identity and rejects cross-feature spec aliases', () => {
|
|
134
592
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-identity-'));
|
|
135
593
|
try {
|
|
@@ -193,17 +651,47 @@ test('validator rejects non-Markdown task artifacts instead of ignoring them', (
|
|
|
193
651
|
}
|
|
194
652
|
});
|
|
195
653
|
|
|
196
|
-
test('validator
|
|
654
|
+
test('validator keeps authoring receipt-free and validates taskless execution closeout proof', () => {
|
|
197
655
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-feature-receipt-'));
|
|
198
656
|
try {
|
|
199
|
-
const specDir = createBoundedSpec(root, 'completed-feature-receipt');
|
|
657
|
+
const specDir = createBoundedSpec(root, 'weak-completed-feature-receipt');
|
|
200
658
|
write(path.join(specDir, 'feature-receipt.md'), [
|
|
201
659
|
'# Feature Verification Receipt', '', 'Verification: PASS',
|
|
202
660
|
'Command: npm test', 'Exit: 0', 'Base: a', 'Head: b',
|
|
203
661
|
].join('\n'));
|
|
204
|
-
const
|
|
205
|
-
assert.notEqual(
|
|
206
|
-
assert.match(output(
|
|
662
|
+
const premature = run(VALIDATOR, [specDir], ROOT);
|
|
663
|
+
assert.notEqual(premature.status, 0);
|
|
664
|
+
assert.match(output(premature), /completed execution receipt is valid only on terminal status done, never for readiness/);
|
|
665
|
+
|
|
666
|
+
const weakSpecPath = path.join(specDir, 'spec.json');
|
|
667
|
+
const weakSpec = JSON.parse(fs.readFileSync(weakSpecPath, 'utf8'));
|
|
668
|
+
weakSpec.status = 'done';
|
|
669
|
+
weakSpec.current_phase = 'done';
|
|
670
|
+
write(weakSpecPath, JSON.stringify(weakSpec, null, 2));
|
|
671
|
+
const weak = run(VALIDATOR, [specDir], ROOT);
|
|
672
|
+
assert.notEqual(weak.status, 0);
|
|
673
|
+
assert.match(output(weak), /completed proof and spec\.json must both use status done/);
|
|
674
|
+
assert.match(output(weak), /observable expected result/);
|
|
675
|
+
assert.match(output(weak), /distinct 40- or 64-hex Base and Head bindings/);
|
|
676
|
+
|
|
677
|
+
const completedDir = createBoundedSpec(root, 'completed-feature-receipt');
|
|
678
|
+
const completedSpecPath = path.join(completedDir, 'spec.json');
|
|
679
|
+
const completedSpec = JSON.parse(fs.readFileSync(completedSpecPath, 'utf8'));
|
|
680
|
+
completedSpec.status = 'done';
|
|
681
|
+
completedSpec.current_phase = 'done';
|
|
682
|
+
write(completedSpecPath, JSON.stringify(completedSpec, null, 2));
|
|
683
|
+
write(path.join(completedDir, 'feature-receipt.md'), [
|
|
684
|
+
'# Feature Verification Receipt', '', 'Verification: PASS', 'Status: done',
|
|
685
|
+
'Command: `node --test test/one.test.js`',
|
|
686
|
+
'Expected result: exit code 0 and one test passes',
|
|
687
|
+
'Observed result: exit code 0 and one test passed',
|
|
688
|
+
'Artifact/runtime proof: `src/one.js` persisted state value 1',
|
|
689
|
+
`Base: ${'a'.repeat(40)}`,
|
|
690
|
+
`Head: ${'b'.repeat(40)}`,
|
|
691
|
+
].join('\n'));
|
|
692
|
+
const completed = run(VALIDATOR, [completedDir], ROOT);
|
|
693
|
+
assert.equal(completed.status, 0, output(completed));
|
|
694
|
+
|
|
207
695
|
} finally {
|
|
208
696
|
fs.rmSync(root, { recursive: true, force: true });
|
|
209
697
|
}
|
|
@@ -222,7 +710,10 @@ test('validator rejects required and optional spec artifacts that symlink outsid
|
|
|
222
710
|
assert.match(output(requiredResult), /requirements\.md: symlink is not allowed/);
|
|
223
711
|
|
|
224
712
|
const optional = createSpec(root, { name: 'optional-artifact-link' });
|
|
225
|
-
|
|
713
|
+
const optionalSpecPath = path.join(optional, 'spec.json');
|
|
714
|
+
const optionalSpec = JSON.parse(fs.readFileSync(optionalSpecPath, 'utf8'));
|
|
715
|
+
optionalSpec.research = 'research.md';
|
|
716
|
+
write(optionalSpecPath, JSON.stringify(optionalSpec, null, 2));
|
|
226
717
|
write(path.join(external, 'research.md'), '# External research\n');
|
|
227
718
|
fs.symlinkSync(path.join(external, 'research.md'), path.join(optional, 'research.md'));
|
|
228
719
|
const optionalResult = run(VALIDATOR, [optional], ROOT);
|
|
@@ -230,7 +721,6 @@ test('validator rejects required and optional spec artifacts that symlink outsid
|
|
|
230
721
|
assert.match(output(optionalResult), /research\.md: symlink is not allowed/);
|
|
231
722
|
|
|
232
723
|
const bounded = createBoundedSpec(root, 'receipt-artifact-link');
|
|
233
|
-
fs.unlinkSync(path.join(bounded, 'feature-receipt.md'));
|
|
234
724
|
write(path.join(external, 'feature-receipt.md'), '# External receipt\n');
|
|
235
725
|
fs.symlinkSync(path.join(external, 'feature-receipt.md'), path.join(bounded, 'feature-receipt.md'));
|
|
236
726
|
const boundedResult = run(VALIDATOR, [bounded], ROOT);
|
|
@@ -242,7 +732,7 @@ test('validator rejects required and optional spec artifacts that symlink outsid
|
|
|
242
732
|
}
|
|
243
733
|
});
|
|
244
734
|
|
|
245
|
-
test('Standard bounded readiness
|
|
735
|
+
test('Standard bounded readiness requires canonical testable requirements and a concrete design boundary', () => {
|
|
246
736
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-standard-placeholders-'));
|
|
247
737
|
try {
|
|
248
738
|
const specDir = createBoundedSpec(root, 'standard-placeholders');
|
|
@@ -252,7 +742,8 @@ test('Standard bounded readiness rejects scope and document placeholders but all
|
|
|
252
742
|
spec.approvals.requirements.user_approved = true;
|
|
253
743
|
spec.approvals.design.user_approved = true;
|
|
254
744
|
spec.validation = { status: 'completed' };
|
|
255
|
-
spec.
|
|
745
|
+
spec.updated_at = '2026-08-11T00:06:00+07:00';
|
|
746
|
+
spec.timestamps = lifecycleTimestamps({ tasks: false });
|
|
256
747
|
spec.scope_lock.source = '{{PROJECT_DESCRIPTION}}';
|
|
257
748
|
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
258
749
|
write(path.join(specDir, 'requirements.md'), '# Requirements\n\n{{REQUIREMENTS}}\n');
|
|
@@ -261,14 +752,36 @@ test('Standard bounded readiness rejects scope and document placeholders but all
|
|
|
261
752
|
const rejected = run(VALIDATOR, [specDir], ROOT);
|
|
262
753
|
assert.notEqual(rejected.status, 0);
|
|
263
754
|
assert.match(output(rejected), /scope_lock\.source.*concrete scope source/);
|
|
264
|
-
assert.match(output(rejected), /requirements\.md: Standard
|
|
265
|
-
assert.match(output(rejected), /design\.md: Standard
|
|
755
|
+
assert.match(output(rejected), /requirements\.md: Standard readiness rejects placeholders/);
|
|
756
|
+
assert.match(output(rejected), /design\.md: Standard readiness rejects placeholders/);
|
|
266
757
|
assert.doesNotMatch(output(rejected), /feature-receipt\.md: canonical feature receipt required/);
|
|
267
758
|
|
|
268
759
|
spec.scope_lock.source = 'The bounded feature scope is the one described here';
|
|
269
760
|
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
270
761
|
write(path.join(specDir, 'requirements.md'), '# Requirements\n\nThe bounded behavior is explicitly defined.\n');
|
|
271
762
|
write(path.join(specDir, 'design.md'), '# Design\n\nThe bounded implementation boundary is explicitly defined.\n');
|
|
763
|
+
const generic = run(VALIDATOR, [specDir], ROOT);
|
|
764
|
+
assert.notEqual(generic.status, 0);
|
|
765
|
+
assert.match(output(generic), /canonical numeric requirement id/);
|
|
766
|
+
assert.match(output(generic), /testable acceptance criterion using RN\.M/);
|
|
767
|
+
assert.match(output(generic), /grounded path\/code identifier/);
|
|
768
|
+
|
|
769
|
+
write(
|
|
770
|
+
path.join(specDir, 'requirements.md'),
|
|
771
|
+
'# Requirements\n\n## Requirements\n\n### Requirement 1: Locale persistence\n\n' +
|
|
772
|
+
'- **R1.1** When a locale is saved, the CLI shall write the selected value to `.codex/runtime.json` and return exit code 0.\n',
|
|
773
|
+
);
|
|
774
|
+
write(
|
|
775
|
+
path.join(specDir, 'design.md'),
|
|
776
|
+
'# Design\n\n## Boundary\n\nThe `.codex/runtime.json` file is owned by `saveLocale`; the CLI calls it before returning success.\n\n' +
|
|
777
|
+
'## Typed Anchors\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n' +
|
|
778
|
+
'| A-D-01 | file | `.codex/runtime.json` | locale persistence target |\n' +
|
|
779
|
+
'| A-D-02 | symbol | `src/runtime.js#saveLocale` | locale writer |\n\n' +
|
|
780
|
+
'## Decisions and Invariants\n\n### D1 — Persist selected locale\n\n- **Decision:** `saveLocale` writes the selected value.\n- **Negative path:** Invalid locale leaves state unchanged.\n- **Anchors:** A-D-01, A-D-02\n\n' +
|
|
781
|
+
'## Verification\n\n| Requirement | Proof target | Expected result | Negative path / reachability |\n|---|---|---|---|\n' +
|
|
782
|
+
'| R1.1 | `node --test test/runtime.test.js` | exit code 0 and saved locale | invalid locale through `saveLocale` |\n',
|
|
783
|
+
);
|
|
784
|
+
completeSemanticReview(specDir);
|
|
272
785
|
const accepted = run(VALIDATOR, [specDir], ROOT);
|
|
273
786
|
assert.equal(accepted.status, 0, output(accepted));
|
|
274
787
|
} finally {
|
|
@@ -280,19 +793,29 @@ test('tasks-only validates the persisted policy before any filesystem mutation',
|
|
|
280
793
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-tasks-only-preflight-'));
|
|
281
794
|
try {
|
|
282
795
|
const standardPolicy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
|
|
796
|
+
const boundaries = JSON.stringify([{
|
|
797
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01'],
|
|
798
|
+
write_sets: { 'R1-01': ['src/one.js'] },
|
|
799
|
+
}]);
|
|
283
800
|
const cases = [
|
|
284
|
-
['malformed', '{', /existing spec\.json is invalid JSON/],
|
|
285
|
-
['null-state', 'null', /existing spec\.json must contain a JSON object/],
|
|
286
|
-
['missing-snapshot', JSON.stringify(
|
|
287
|
-
['null-snapshot',
|
|
288
|
-
['extra-field',
|
|
801
|
+
['malformed', () => '{', /existing spec\.json is invalid JSON/],
|
|
802
|
+
['null-state', () => 'null', /existing spec\.json must contain a JSON object/],
|
|
803
|
+
['missing-snapshot', (state) => { delete state.workflow_policy; return JSON.stringify(state, null, 2); }, /existing spec must contain a persisted workflow_policy/],
|
|
804
|
+
['null-snapshot', (state) => { state.workflow_policy = null; return JSON.stringify(state, null, 2); }, /existing workflow_policy is invalid/],
|
|
805
|
+
['extra-field', (state) => {
|
|
806
|
+
state.workflow_policy = { ...standardPolicy, extra: true };
|
|
807
|
+
return JSON.stringify(state, null, 2);
|
|
808
|
+
}, /schema 2\.0 workflow_policy contains unsupported legacy authority field\(s\): extra/],
|
|
289
809
|
];
|
|
290
|
-
for (const [name,
|
|
291
|
-
const specDir =
|
|
810
|
+
for (const [name, mutate, expected] of cases) {
|
|
811
|
+
const specDir = createLegacyTasklessSpec20(root, name, standardPolicy);
|
|
292
812
|
const specPath = path.join(specDir, 'spec.json');
|
|
293
|
-
|
|
813
|
+
const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
814
|
+
write(specPath, mutate(state));
|
|
294
815
|
const before = fs.readFileSync(specPath, 'utf8');
|
|
295
|
-
const result = run(SCAFFOLD, [
|
|
816
|
+
const result = run(SCAFFOLD, [
|
|
817
|
+
name, '--tasks', 'R1-01-one', '--tasks-only', '--boundaries', boundaries,
|
|
818
|
+
], root);
|
|
296
819
|
assert.equal(result.status, 2, output(result));
|
|
297
820
|
assert.match(output(result), expected);
|
|
298
821
|
assert.equal(fs.readFileSync(specPath, 'utf8'), before);
|
|
@@ -314,6 +837,21 @@ test('scaffold accepts only one safe alphanumeric-starting feature segment', ()
|
|
|
314
837
|
assert.match(output(result), /feature|safe relative|path segment/i);
|
|
315
838
|
assert.equal(snapshotTree(root), before, `${feature} must not mutate the workspace`);
|
|
316
839
|
}
|
|
840
|
+
|
|
841
|
+
const beforeFreshSequence = snapshotTree(root);
|
|
842
|
+
const badFreshSequence = run(SCAFFOLD, ['bad-fresh-sequence', '--tasks', 'R1-1-one'], root);
|
|
843
|
+
assert.equal(badFreshSequence.status, 2, output(badFreshSequence));
|
|
844
|
+
assert.match(output(badFreshSequence), /SEQ 2 digits/);
|
|
845
|
+
assert.equal(snapshotTree(root), beforeFreshSequence, 'one-digit fresh task sequence mutated the workspace');
|
|
846
|
+
|
|
847
|
+
createLegacyTasklessSpec20(root, 'bad-tasks-only-sequence', POLICY.workflowPolicySnapshot({ riskSignals: {} }));
|
|
848
|
+
const beforeTasksOnlySequence = snapshotTree(root);
|
|
849
|
+
const badTasksOnlySequence = run(SCAFFOLD, [
|
|
850
|
+
'bad-tasks-only-sequence', '--tasks', 'R1-1-one', '--tasks-only',
|
|
851
|
+
], root);
|
|
852
|
+
assert.equal(badTasksOnlySequence.status, 2, output(badTasksOnlySequence));
|
|
853
|
+
assert.match(output(badTasksOnlySequence), /SEQ 2 digits/);
|
|
854
|
+
assert.equal(snapshotTree(root), beforeTasksOnlySequence, 'one-digit tasks-only sequence mutated the workspace');
|
|
317
855
|
} finally {
|
|
318
856
|
fs.rmSync(root, { recursive: true, force: true });
|
|
319
857
|
}
|
|
@@ -328,9 +866,9 @@ test('validator requires a strict persisted workflow-policy snapshot and never f
|
|
|
328
866
|
['explicit-null', (state) => { state.workflow_policy = null; }, /workflow_policy must be an object/],
|
|
329
867
|
['malformed', (state) => { state.workflow_policy = { ...policy, proof_obligations: { needsExecutionProof: true } }; }, /proof_obligations must be an array/],
|
|
330
868
|
['extra-field', (state) => { state.workflow_policy = { ...policy, extra: true }; }, /workflow_policy fields must be exactly/],
|
|
331
|
-
['lane-mismatch', (state) => { state.workflow_policy = { ...policy, lane: '
|
|
332
|
-
['risk-mismatch', (state) => { state.workflow_policy = { ...policy, risks: ['auth'] }; }, /
|
|
333
|
-
['obligation-mismatch', (state) => { state.workflow_policy = { ...policy, proof_obligations: ['needsExecutionProof'], actor_needs: [{ capability: 'execution-proof', independence: 'same-session' }] }; }, /proof_obligations do not match
|
|
869
|
+
['lane-mismatch', (state) => { state.workflow_policy = { ...policy, lane: 'Critical', automatic_lane: 'Critical' }; }, /lane is not the derived compatibility lane|automatic_lane is not the derived compatibility lane/],
|
|
870
|
+
['risk-mismatch', (state) => { state.workflow_policy = { ...policy, risks: ['auth'] }; }, /assurance_level must be at least Elevated/],
|
|
871
|
+
['obligation-mismatch', (state) => { state.workflow_policy = { ...policy, proof_obligations: ['needsInspection', 'needsExecutionProof'], actor_needs: [{ capability: 'inspection', independence: 'same-session' }, { capability: 'execution-proof', independence: 'same-session' }] }; }, /proof_obligations do not match assurance_level and risks/],
|
|
334
872
|
];
|
|
335
873
|
for (const [name, mutate, expected] of cases) {
|
|
336
874
|
const specDir = createSpec(root, { name, spec: { workflow_policy: policy } });
|
|
@@ -357,7 +895,8 @@ test('validator requires a strict persisted workflow-policy snapshot and never f
|
|
|
357
895
|
tasks: { generated: true, agent_validated: true, user_approved: true },
|
|
358
896
|
},
|
|
359
897
|
validation: { status: 'completed' },
|
|
360
|
-
|
|
898
|
+
updated_at: '2026-08-11T00:06:00+07:00',
|
|
899
|
+
timestamps: lifecycleTimestamps(),
|
|
361
900
|
},
|
|
362
901
|
});
|
|
363
902
|
const legacyBefore = snapshotTree(legacyDir);
|
|
@@ -388,40 +927,66 @@ test('tasks-only preflights malformed topology, duplicates, and symlinks without
|
|
|
388
927
|
['unsafe-research-path', { workflow_policy: policy, research: '../outside' }, /spec\.research must be a safe relative path/],
|
|
389
928
|
];
|
|
390
929
|
for (const [name, state, expected] of cases) {
|
|
391
|
-
const specDir =
|
|
392
|
-
write(path.join(specDir, 'spec.json'), JSON.stringify(
|
|
930
|
+
const specDir = createLegacyTasklessSpec20(root, name, policy);
|
|
931
|
+
write(path.join(specDir, 'spec.json'), JSON.stringify({
|
|
932
|
+
...legacyTasklessSpec20State(name, policy),
|
|
933
|
+
...state,
|
|
934
|
+
}, null, 2));
|
|
935
|
+
for (const declaredTask of new Set(Array.isArray(state.task_files) ? state.task_files : [])) {
|
|
936
|
+
write(path.join(specDir, declaredTask), task());
|
|
937
|
+
}
|
|
938
|
+
const boundaryTasks = Array.isArray(state.task_files) && state.task_files.includes(taskPath)
|
|
939
|
+
? ['R1-01', 'R1-02', 'R1-03'] : ['R1-02', 'R1-03'];
|
|
940
|
+
const boundaries = JSON.stringify([{
|
|
941
|
+
id: 'B-OWN', type: 'ownership', tasks: boundaryTasks,
|
|
942
|
+
write_sets: Object.fromEntries(boundaryTasks.map((id) => [id, [`src/${id.toLowerCase()}.js`]])),
|
|
943
|
+
}]);
|
|
393
944
|
const before = snapshotTree(root);
|
|
394
|
-
const result = run(SCAFFOLD, [
|
|
945
|
+
const result = run(SCAFFOLD, [
|
|
946
|
+
name, '--tasks', 'R1-02-two,R1-03-three', '--tasks-only',
|
|
947
|
+
'--boundaries', boundaries,
|
|
948
|
+
], root);
|
|
395
949
|
assert.equal(result.status, 2, output(result));
|
|
396
950
|
assert.match(output(result), expected);
|
|
397
951
|
assert.equal(snapshotTree(root), before, `${name} mutated the existing topology`);
|
|
398
952
|
}
|
|
399
953
|
|
|
400
|
-
const conflictDir =
|
|
954
|
+
const conflictDir = createLegacyTasklessSpec20(root, 'conflicting-duplicate', policy);
|
|
401
955
|
write(path.join(conflictDir, 'tasks/task-R1-01-one.md'), 'already filled\n');
|
|
402
956
|
write(path.join(conflictDir, 'spec.json'), JSON.stringify({
|
|
403
|
-
|
|
957
|
+
...legacyTasklessSpec20State('conflicting-duplicate', policy),
|
|
404
958
|
task_files: [taskPath],
|
|
405
959
|
task_registry: { [taskPath]: { ...registryEntry, title: 'Conflicting title' } },
|
|
406
960
|
}, null, 2));
|
|
407
961
|
const conflictBefore = snapshotTree(root);
|
|
408
|
-
const
|
|
962
|
+
const singleBoundary = JSON.stringify([{
|
|
963
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01'], write_sets: { 'R1-01': ['src/one.js'] },
|
|
964
|
+
}]);
|
|
965
|
+
const conflict = run(SCAFFOLD, [
|
|
966
|
+
'conflicting-duplicate', '--tasks', 'R1-01-one', '--tasks-only',
|
|
967
|
+
'--boundaries', singleBoundary,
|
|
968
|
+
], root);
|
|
409
969
|
assert.equal(conflict.status, 2, output(conflict));
|
|
410
970
|
assert.match(output(conflict), /conflicting duplicate/);
|
|
411
971
|
assert.equal(snapshotTree(root), conflictBefore);
|
|
412
972
|
|
|
413
|
-
const duplicateDir =
|
|
973
|
+
const duplicateDir = createLegacyTasklessSpec20(root, 'duplicate-cli', policy);
|
|
414
974
|
const duplicateBefore = snapshotTree(root);
|
|
415
|
-
const duplicate = run(SCAFFOLD, [
|
|
975
|
+
const duplicate = run(SCAFFOLD, [
|
|
976
|
+
'duplicate-cli', '--tasks', 'R1-01-one,R1-01-two', '--tasks-only',
|
|
977
|
+
'--boundaries', singleBoundary,
|
|
978
|
+
], root);
|
|
416
979
|
assert.equal(duplicate.status, 2, output(duplicate));
|
|
417
980
|
assert.match(output(duplicate), /duplicate task id.*R1-01/);
|
|
418
981
|
assert.equal(snapshotTree(root), duplicateBefore);
|
|
419
982
|
|
|
420
|
-
const symlinkDir =
|
|
983
|
+
const symlinkDir = createLegacyTasklessSpec20(root, 'symlink-tasks', policy);
|
|
421
984
|
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-symlink-target-'));
|
|
422
985
|
fs.symlinkSync(outside, path.join(symlinkDir, 'tasks'), 'dir');
|
|
423
986
|
const symlinkBefore = snapshotTree(root);
|
|
424
|
-
const symlinkResult = run(SCAFFOLD, [
|
|
987
|
+
const symlinkResult = run(SCAFFOLD, [
|
|
988
|
+
'symlink-tasks', '--tasks', 'R1-01-one', '--tasks-only', '--boundaries', singleBoundary,
|
|
989
|
+
], root);
|
|
425
990
|
assert.equal(symlinkResult.status, 2, output(symlinkResult));
|
|
426
991
|
assert.match(output(symlinkResult), /symlink/);
|
|
427
992
|
assert.equal(snapshotTree(root), symlinkBefore);
|
|
@@ -435,11 +1000,14 @@ test('tasks-only commits transactionally and preserves exact bytes after an inje
|
|
|
435
1000
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-transaction-'));
|
|
436
1001
|
try {
|
|
437
1002
|
const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
|
|
438
|
-
|
|
1003
|
+
createLegacyTasklessSpec20(root, 'partial-write', policy);
|
|
439
1004
|
const before = snapshotTree(root);
|
|
440
1005
|
const result = run(
|
|
441
1006
|
SCAFFOLD,
|
|
442
|
-
['partial-write', '--tasks', 'R1-01-one,R1-02-two', '--
|
|
1007
|
+
['partial-write', '--tasks', 'R1-01-one,R1-02-two', '--boundaries', JSON.stringify([{
|
|
1008
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
1009
|
+
write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
|
|
1010
|
+
}]), '--tasks-only'],
|
|
443
1011
|
root,
|
|
444
1012
|
{ env: { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '1' } },
|
|
445
1013
|
);
|
|
@@ -451,51 +1019,261 @@ test('tasks-only commits transactionally and preserves exact bytes after an inje
|
|
|
451
1019
|
}
|
|
452
1020
|
});
|
|
453
1021
|
|
|
1022
|
+
test('tasks-only migration derives only absent feature identity and rejects present conflicts atomically', () => {
|
|
1023
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-legacy-identity-'));
|
|
1024
|
+
const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
|
|
1025
|
+
const boundaries = JSON.stringify([{
|
|
1026
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
1027
|
+
write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
|
|
1028
|
+
}]);
|
|
1029
|
+
const migrate = (feature) => run(SCAFFOLD, [
|
|
1030
|
+
feature, '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', '--boundaries', boundaries,
|
|
1031
|
+
], root);
|
|
1032
|
+
try {
|
|
1033
|
+
const absentFeature = 'legacy-identity-absent';
|
|
1034
|
+
const absentDir = createLegacyTasklessSpec20(root, absentFeature, policy);
|
|
1035
|
+
const absentPath = path.join(absentDir, 'spec.json');
|
|
1036
|
+
const absentState = JSON.parse(fs.readFileSync(absentPath, 'utf8'));
|
|
1037
|
+
delete absentState.feature_name;
|
|
1038
|
+
write(absentPath, JSON.stringify(absentState, null, 2));
|
|
1039
|
+
|
|
1040
|
+
const migrated = migrate(absentFeature);
|
|
1041
|
+
assert.equal(migrated.status, 0, output(migrated));
|
|
1042
|
+
const canonical = JSON.parse(fs.readFileSync(absentPath, 'utf8'));
|
|
1043
|
+
assert.equal(canonical.schema_version, '2.1');
|
|
1044
|
+
assert.equal(canonical.feature_name, absentFeature);
|
|
1045
|
+
assert.equal(canonical.created_at, '2026-08-11T00:00:00+07:00');
|
|
1046
|
+
assert.equal(canonical.language, 'en');
|
|
1047
|
+
assert.equal(canonical.status, 'in_progress');
|
|
1048
|
+
assert.deepEqual(canonical.scope_lock, absentState.scope_lock);
|
|
1049
|
+
assert.deepEqual(canonical.authoring, {
|
|
1050
|
+
requirements: 'draft', design: 'draft', research: 'absent', tasks: 'draft',
|
|
1051
|
+
});
|
|
1052
|
+
assert.equal(canonical.ready_for_implementation, false);
|
|
1053
|
+
assert.equal(canonical.validation.status, 'not-run');
|
|
1054
|
+
assert.equal(canonical.validation.semantic_review.status, 'not-run');
|
|
1055
|
+
assert.equal(Object.hasOwn(canonical, 'research'), false);
|
|
1056
|
+
assert.equal(fs.existsSync(path.join(absentDir, 'research.md')), false);
|
|
1057
|
+
assert.equal(fs.existsSync(path.join(absentDir, 'receipts')), false);
|
|
1058
|
+
assert.equal(fs.existsSync(path.join(absentDir, 'reports')), false);
|
|
1059
|
+
for (const entry of Object.values(canonical.task_registry)) {
|
|
1060
|
+
assert.deepEqual(
|
|
1061
|
+
[entry.status, entry.blocker, entry.started_at, entry.completed_at, entry.last_updated_at],
|
|
1062
|
+
['pending', null, null, null, null],
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
for (const [label, featureName] of [
|
|
1067
|
+
['empty', ''], ['non-string', 42], ['different', 'another-feature'],
|
|
1068
|
+
]) {
|
|
1069
|
+
const feature = `legacy-identity-${label}`;
|
|
1070
|
+
const specDir = createLegacyTasklessSpec20(root, feature, policy);
|
|
1071
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
1072
|
+
const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
1073
|
+
state.feature_name = featureName;
|
|
1074
|
+
write(specPath, JSON.stringify(state, null, 2));
|
|
1075
|
+
const before = snapshotTree(specDir);
|
|
1076
|
+
const rejected = migrate(feature);
|
|
1077
|
+
assert.equal(rejected.status, 2, `${label}: ${output(rejected)}`);
|
|
1078
|
+
assert.match(output(rejected), /feature_name conflicts with the scaffold feature identity/);
|
|
1079
|
+
assert.equal(snapshotTree(specDir), before, `${label} identity rejection mutated the fixture`);
|
|
1080
|
+
}
|
|
1081
|
+
} finally {
|
|
1082
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
test('fresh scaffold rolls back authority rejection and injected partial writes without deleting pre-existing content', () => {
|
|
1087
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-fresh-transaction-'));
|
|
1088
|
+
try {
|
|
1089
|
+
const codexRoot = path.join(root, '.codex');
|
|
1090
|
+
const installedScripts = path.join(codexRoot, 'scripts');
|
|
1091
|
+
const installedHooks = path.join(codexRoot, 'hooks');
|
|
1092
|
+
const installedTemplates = path.join(codexRoot, 'skills/specs/templates');
|
|
1093
|
+
fs.mkdirSync(installedScripts, { recursive: true });
|
|
1094
|
+
fs.mkdirSync(installedHooks, { recursive: true });
|
|
1095
|
+
fs.mkdirSync(path.dirname(installedTemplates), { recursive: true });
|
|
1096
|
+
fs.cpSync(path.join(ROOT, 'src/claude/skills/specs/templates'), installedTemplates, { recursive: true });
|
|
1097
|
+
copyCodexRuntime(codexRoot, ['scripts/spec-scaffold.cjs']);
|
|
1098
|
+
write(path.join(installedHooks, 'completion-authority-state.cjs'), 'module.exports = {};\n');
|
|
1099
|
+
write(
|
|
1100
|
+
path.join(installedHooks, 'completion-authority-check.cjs'),
|
|
1101
|
+
'module.exports = { observePolicyBaseline() { return { ok: false, reason: "fixture authority reject" }; } };\n',
|
|
1102
|
+
);
|
|
1103
|
+
write(path.join(root, 'specs/keep.txt'), 'pre-existing\n');
|
|
1104
|
+
|
|
1105
|
+
const beforeAuthority = snapshotTree(root);
|
|
1106
|
+
const authority = run(
|
|
1107
|
+
path.join(installedScripts, 'spec-scaffold.cjs'),
|
|
1108
|
+
['authority-reject', '--lane', 'Standard'],
|
|
1109
|
+
root,
|
|
1110
|
+
);
|
|
1111
|
+
assert.equal(authority.status, 2, output(authority));
|
|
1112
|
+
assert.match(output(authority), /fixture authority reject.*transaction rolled back/);
|
|
1113
|
+
assert.equal(snapshotTree(root), beforeAuthority);
|
|
1114
|
+
assert.equal(fs.existsSync(path.join(root, 'specs/authority-reject')), false);
|
|
1115
|
+
|
|
1116
|
+
write(
|
|
1117
|
+
path.join(installedHooks, 'completion-authority-check.cjs'),
|
|
1118
|
+
'module.exports = { observePolicyBaseline() { return { ok: true }; } };\n',
|
|
1119
|
+
);
|
|
1120
|
+
const seeded = run(
|
|
1121
|
+
path.join(installedScripts, 'spec-scaffold.cjs'),
|
|
1122
|
+
['tasks-authority', '--lane', 'Standard'],
|
|
1123
|
+
root,
|
|
1124
|
+
);
|
|
1125
|
+
assert.equal(seeded.status, 0, output(seeded));
|
|
1126
|
+
write(
|
|
1127
|
+
path.join(installedHooks, 'completion-authority-check.cjs'),
|
|
1128
|
+
'module.exports = { observePolicyBaseline() { return { ok: false, reason: "fixture tasks-only authority reject" }; } };\n',
|
|
1129
|
+
);
|
|
1130
|
+
const beforeTasksAuthority = snapshotTree(root);
|
|
1131
|
+
const tasksAuthority = run(
|
|
1132
|
+
path.join(installedScripts, 'spec-scaffold.cjs'),
|
|
1133
|
+
['tasks-authority', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only',
|
|
1134
|
+
'--boundaries', JSON.stringify([{
|
|
1135
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
1136
|
+
write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
|
|
1137
|
+
}])],
|
|
1138
|
+
root,
|
|
1139
|
+
);
|
|
1140
|
+
assert.equal(tasksAuthority.status, 2, output(tasksAuthority));
|
|
1141
|
+
assert.match(output(tasksAuthority), /fixture tasks-only authority reject.*transaction rolled back/);
|
|
1142
|
+
assert.equal(snapshotTree(root), beforeTasksAuthority);
|
|
1143
|
+
assert.equal(fs.existsSync(path.join(root, 'specs/tasks-authority/tasks/task-R1-01-one.md')), false);
|
|
1144
|
+
|
|
1145
|
+
const beforeInjected = snapshotTree(root);
|
|
1146
|
+
const injected = run(
|
|
1147
|
+
SCAFFOLD,
|
|
1148
|
+
['fresh-injected', '--lane', 'Standard'],
|
|
1149
|
+
root,
|
|
1150
|
+
{ env: { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '2' } },
|
|
1151
|
+
);
|
|
1152
|
+
assert.equal(injected.status, 2, output(injected));
|
|
1153
|
+
assert.match(output(injected), /injected failure.*transaction rolled back/);
|
|
1154
|
+
assert.equal(snapshotTree(root), beforeInjected);
|
|
1155
|
+
assert.equal(fs.readFileSync(path.join(root, 'specs/keep.txt'), 'utf8'), 'pre-existing\n');
|
|
1156
|
+
} finally {
|
|
1157
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
test('installed Codex scaffold rejects a symlinked template parent before creating a spec', () => {
|
|
1162
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-template-symlink-'));
|
|
1163
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-template-outside-'));
|
|
1164
|
+
try {
|
|
1165
|
+
const codexRoot = path.join(root, '.codex');
|
|
1166
|
+
copyCodexRuntime(codexRoot, ['scripts/spec-scaffold.cjs']);
|
|
1167
|
+
write(
|
|
1168
|
+
path.join(codexRoot, 'hooks/completion-authority-check.cjs'),
|
|
1169
|
+
'module.exports = { observePolicyBaseline() { return { ok: true }; } };\n',
|
|
1170
|
+
);
|
|
1171
|
+
write(path.join(codexRoot, 'hooks/completion-authority-state.cjs'), 'module.exports = {};\n');
|
|
1172
|
+
fs.cpSync(path.join(ROOT, 'src/claude/skills/specs/templates'), outside, { recursive: true });
|
|
1173
|
+
const templatesParent = path.join(root, '.agents/skills/specs');
|
|
1174
|
+
fs.mkdirSync(templatesParent, { recursive: true });
|
|
1175
|
+
fs.symlinkSync(outside, path.join(templatesParent, 'templates'));
|
|
1176
|
+
|
|
1177
|
+
const result = run(
|
|
1178
|
+
path.join(codexRoot, 'scripts/spec-scaffold.cjs'),
|
|
1179
|
+
['template-parent-escape', '--lane', 'Standard'],
|
|
1180
|
+
root,
|
|
1181
|
+
);
|
|
1182
|
+
assert.equal(result.status, 2, output(result));
|
|
1183
|
+
assert.match(output(result), /template check failed|symlink component rejected/i);
|
|
1184
|
+
assert.equal(fs.existsSync(path.join(root, 'specs/template-parent-escape')), false);
|
|
1185
|
+
} finally {
|
|
1186
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1187
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
|
|
454
1191
|
test('tasks-only is idempotent for an identical task request and preserves the snapshot bytes', () => {
|
|
455
1192
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-idempotent-'));
|
|
456
1193
|
try {
|
|
457
1194
|
const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
|
|
458
|
-
|
|
459
|
-
const
|
|
1195
|
+
createLegacyTasklessSpec20(root, 'idempotent', policy);
|
|
1196
|
+
const boundaryArgs = ['--boundaries', JSON.stringify([{
|
|
1197
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
1198
|
+
write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
|
|
1199
|
+
}])];
|
|
1200
|
+
const idempotentArgs = ['idempotent', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', ...boundaryArgs];
|
|
1201
|
+
const first = run(SCAFFOLD, idempotentArgs, root);
|
|
460
1202
|
assert.equal(first.status, 0, output(first));
|
|
461
1203
|
const beforeSecond = snapshotTree(root);
|
|
462
|
-
const second = run(SCAFFOLD,
|
|
1204
|
+
const second = run(SCAFFOLD, idempotentArgs, root);
|
|
463
1205
|
assert.equal(second.status, 0, output(second));
|
|
464
1206
|
assert.match(output(second), /0 new task stub\(s\)/);
|
|
465
1207
|
assert.equal(snapshotTree(root), beforeSecond);
|
|
1208
|
+
|
|
1209
|
+
const artifactBase = run(SCAFFOLD, ['idempotent-artifact', '--lane', 'Standard'], root);
|
|
1210
|
+
assert.equal(artifactBase.status, 0, output(artifactBase));
|
|
1211
|
+
const proofArgs = ['--boundaries', JSON.stringify([{
|
|
1212
|
+
id: 'B-V', type: 'proof', subject: 'R1-01', verifier: 'R1-02',
|
|
1213
|
+
verification_ref: 'V1', artifact_anchor: 'A-R1-02-02',
|
|
1214
|
+
}])];
|
|
1215
|
+
const artifactArgs = [
|
|
1216
|
+
'idempotent-artifact', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', ...proofArgs,
|
|
1217
|
+
];
|
|
1218
|
+
const artifactFirst = run(SCAFFOLD, artifactArgs, root);
|
|
1219
|
+
assert.equal(artifactFirst.status, 0, output(artifactFirst));
|
|
1220
|
+
const artifactDir = path.join(root, 'specs/idempotent-artifact');
|
|
1221
|
+
completeScaffoldedSpec21(artifactDir);
|
|
1222
|
+
const artifactValidation = run(VALIDATOR, [artifactDir], ROOT);
|
|
1223
|
+
assert.equal(artifactValidation.status, 0, output(artifactValidation));
|
|
1224
|
+
const beforeArtifactSecond = snapshotTree(root);
|
|
1225
|
+
const artifactSecond = run(SCAFFOLD, artifactArgs, root);
|
|
1226
|
+
assert.equal(artifactSecond.status, 0, output(artifactSecond));
|
|
1227
|
+
assert.match(output(artifactSecond), /0 new task stub\(s\)/);
|
|
1228
|
+
assert.equal(snapshotTree(root), beforeArtifactSecond);
|
|
466
1229
|
} finally {
|
|
467
1230
|
fs.rmSync(root, { recursive: true, force: true });
|
|
468
1231
|
}
|
|
469
1232
|
});
|
|
470
1233
|
|
|
471
|
-
test('tasks-only consumes the policy snapshot and escalates
|
|
1234
|
+
test('tasks-only consumes the policy snapshot and escalates auth to Elevated monotonically', () => {
|
|
472
1235
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-tasks-only-policy-'));
|
|
473
1236
|
try {
|
|
474
|
-
const specDir =
|
|
1237
|
+
const specDir = createLegacyTasklessSpec20(root, 'existing', POLICY.workflowPolicySnapshot({ riskSignals: {} }));
|
|
1238
|
+
const boundaries = JSON.stringify([{
|
|
1239
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
1240
|
+
write_sets: { 'R1-01': ['src/auth.js'], 'R1-02': ['src/next.js'] },
|
|
1241
|
+
}]);
|
|
475
1242
|
const escalated = run(SCAFFOLD, [
|
|
476
|
-
'existing', '--tasks', 'R1-01-auth', '--risks', 'auth', '--tasks-only',
|
|
1243
|
+
'existing', '--tasks', 'R1-01-auth,R1-02-next', '--risks', 'auth', '--tasks-only',
|
|
1244
|
+
'--boundaries', boundaries,
|
|
477
1245
|
], root);
|
|
478
1246
|
assert.equal(escalated.status, 0, output(escalated));
|
|
479
|
-
assert.match(output(escalated), /
|
|
480
|
-
assert.
|
|
1247
|
+
assert.match(output(escalated), /2 new task stub\(s\)/);
|
|
1248
|
+
assert.doesNotMatch(output(escalated), /research\.md created/);
|
|
481
1249
|
const first = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
482
|
-
assert.equal(first.
|
|
1250
|
+
assert.equal(POLICY.readWorkflowPolicySnapshot(first).lane, 'Standard');
|
|
1251
|
+
assert.equal(POLICY.readWorkflowPolicySnapshot(first).assurance_level, 'Elevated');
|
|
483
1252
|
assert.deepEqual(first.workflow_policy.risks, ['auth']);
|
|
484
|
-
assert.equal(first.
|
|
485
|
-
assert.
|
|
486
|
-
assert.equal(fs.existsSync(path.join(specDir, 'research.md')),
|
|
1253
|
+
assert.equal(POLICY.readWorkflowPolicySnapshot(first).artifact_profile, 'bounded');
|
|
1254
|
+
assert.equal(POLICY.readWorkflowPolicySnapshot(first).proof_obligations.includes('needsResearchGrounding'), false);
|
|
1255
|
+
assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
|
|
487
1256
|
assert.equal(fs.existsSync(path.join(specDir, 'reports')), false);
|
|
488
1257
|
|
|
489
1258
|
const policyBytes = JSON.stringify(first.workflow_policy);
|
|
490
|
-
const noNewRisk = run(SCAFFOLD, [
|
|
1259
|
+
const noNewRisk = run(SCAFFOLD, [
|
|
1260
|
+
'existing', '--tasks', 'R1-01-auth,R1-02-next', '--tasks-only', '--boundaries', boundaries,
|
|
1261
|
+
], root);
|
|
491
1262
|
assert.equal(noNewRisk.status, 0, output(noNewRisk));
|
|
492
|
-
assert.match(output(noNewRisk), /
|
|
1263
|
+
assert.match(output(noNewRisk), /0 new task stub\(s\)/);
|
|
493
1264
|
const second = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
494
1265
|
assert.equal(JSON.stringify(second.workflow_policy), policyBytes);
|
|
495
1266
|
|
|
496
1267
|
const beforeRejectedLane = fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8');
|
|
1268
|
+
const rejectedBoundaries = JSON.stringify([{
|
|
1269
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02', 'R1-03'],
|
|
1270
|
+
write_sets: {
|
|
1271
|
+
'R1-01': ['src/auth.js'], 'R1-02': ['src/next.js'], 'R1-03': ['src/invalid.js'],
|
|
1272
|
+
},
|
|
1273
|
+
}]);
|
|
497
1274
|
const rejectedLane = run(SCAFFOLD, [
|
|
498
|
-
'existing', '--tasks', 'R1-03-invalid', '--lane', '
|
|
1275
|
+
'existing', '--tasks', 'R1-03-invalid', '--lane', 'Critical', '--tasks-only',
|
|
1276
|
+
'--boundaries', rejectedBoundaries,
|
|
499
1277
|
], root);
|
|
500
1278
|
assert.equal(rejectedLane.status, 2, output(rejectedLane));
|
|
501
1279
|
assert.match(output(rejectedLane), /does not match persisted workflow_policy\.lane/);
|
|
@@ -506,54 +1284,25 @@ test('tasks-only consumes the policy snapshot and escalates Standard auth monoto
|
|
|
506
1284
|
}
|
|
507
1285
|
});
|
|
508
1286
|
|
|
509
|
-
test('scaffold emits
|
|
1287
|
+
test('scaffold emits minimal 2.1 artifacts without default research or reports ceremony', () => {
|
|
510
1288
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-artifact-profile-'));
|
|
511
1289
|
try {
|
|
512
1290
|
const standardDefault = run(SCAFFOLD, ['standard-default', '--lane', 'Standard'], root);
|
|
513
1291
|
assert.equal(standardDefault.status, 0, output(standardDefault));
|
|
514
1292
|
const standardDir = path.join(root, 'specs', 'standard-default');
|
|
515
1293
|
assert.deepEqual(fs.readdirSync(standardDir).sort(), [
|
|
516
|
-
'design.md', '
|
|
1294
|
+
'design.md', 'requirements.md', 'spec.json',
|
|
517
1295
|
]);
|
|
518
1296
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
assert.equal(fs.existsSync(path.join(specDir, 'tasks/task-R1-01-one.md')), true);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
for (const [name, risk] of [['critical-auth-case', 'Auth'], ['critical-auth-alias', 'authentication']]) {
|
|
532
|
-
const result = run(SCAFFOLD, [name, '--risks', risk, '--tasks', 'R1-01-one'], root);
|
|
533
|
-
assert.equal(result.status, 0, output(result));
|
|
534
|
-
const specDir = path.join(root, 'specs', name);
|
|
535
|
-
const spec = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
536
|
-
assert.equal(spec.workflow_policy.lane, 'Critical');
|
|
537
|
-
assert.equal(fs.existsSync(path.join(specDir, 'research.md')), true);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const unknown = run(SCAFFOLD, ['unknown-risk', '--risks', 'unclassified-risk'], root);
|
|
541
|
-
assert.equal(unknown.status, 0, output(unknown));
|
|
542
|
-
const unknownSpec = JSON.parse(fs.readFileSync(path.join(root, 'specs/unknown-risk/spec.json'), 'utf8'));
|
|
543
|
-
assert.equal(unknownSpec.workflow_policy.lane, 'Standard');
|
|
544
|
-
assert.equal(fs.existsSync(path.join(root, 'specs/unknown-risk/research.md')), false);
|
|
545
|
-
|
|
546
|
-
const authTemplate = run(SCAFFOLD, [
|
|
547
|
-
'critical-auth-template', '--risks', 'auth', '--tasks', 'R1-01-artifact',
|
|
548
|
-
], root);
|
|
549
|
-
assert.equal(authTemplate.status, 0, output(authTemplate));
|
|
550
|
-
const authTemplateDir = path.join(root, 'specs', 'critical-auth-template');
|
|
551
|
-
const unfilled = run(VALIDATOR, [authTemplateDir], ROOT);
|
|
552
|
-
assert.notEqual(unfilled.status, 0);
|
|
553
|
-
assert.match(output(unfilled), /Evidence Summary must contain concrete evidence/);
|
|
554
|
-
completeScaffoldedSpec(authTemplateDir);
|
|
555
|
-
const filled = run(VALIDATOR, [authTemplateDir], ROOT);
|
|
556
|
-
assert.equal(filled.status, 0, output(filled));
|
|
1297
|
+
const strict = run(SCAFFOLD, ['strict-taskless', '--risks', 'auth', '--assurance-level', 'Strict'], root);
|
|
1298
|
+
assert.equal(strict.status, 0, output(strict));
|
|
1299
|
+
const strictDir = path.join(root, 'specs', 'strict-taskless');
|
|
1300
|
+
const strictSpec = JSON.parse(fs.readFileSync(path.join(strictDir, 'spec.json'), 'utf8'));
|
|
1301
|
+
assert.equal(strictSpec.schema_version, '2.1');
|
|
1302
|
+
assert.equal(POLICY.readWorkflowPolicySnapshot(strictSpec).lane, 'Critical');
|
|
1303
|
+
assert.equal(strictSpec.authoring.tasks, 'absent');
|
|
1304
|
+
assert.equal(fs.existsSync(path.join(strictDir, 'research.md')), false);
|
|
1305
|
+
assert.equal(fs.existsSync(path.join(strictDir, 'reports')), false);
|
|
557
1306
|
} finally {
|
|
558
1307
|
fs.rmSync(root, { recursive: true, force: true });
|
|
559
1308
|
}
|
|
@@ -562,7 +1311,7 @@ test('scaffold emits bounded artifacts on demand without default research or rep
|
|
|
562
1311
|
test('strict validator requires numeric IDs, rejects phantom mappings, and grounds research content', () => {
|
|
563
1312
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-strict-grounding-'));
|
|
564
1313
|
try {
|
|
565
|
-
const strictPolicy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
|
|
1314
|
+
const strictPolicy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
|
|
566
1315
|
const noIds = createSpec(root, {
|
|
567
1316
|
name: 'no-numeric-ids',
|
|
568
1317
|
requirements: '# Requirements\n\nThe behavior has no numeric identifier.\n',
|
|
@@ -572,6 +1321,15 @@ test('strict validator requires numeric IDs, rejects phantom mappings, and groun
|
|
|
572
1321
|
assert.notEqual(noIdsResult.status, 0);
|
|
573
1322
|
assert.match(output(noIdsResult), /strict workflow requires numeric requirement IDs/);
|
|
574
1323
|
|
|
1324
|
+
const legacyDialect = createSpec(root, {
|
|
1325
|
+
name: 'legacy-req-dialect',
|
|
1326
|
+
requirements: '# Requirements\n\n## Requirements\n\n### REQ-01: One\n\n- **REQ-01.1** The service shall return one result.\n',
|
|
1327
|
+
spec: { workflow_policy: strictPolicy },
|
|
1328
|
+
});
|
|
1329
|
+
const legacyDialectResult = run(VALIDATOR, [legacyDialect], ROOT);
|
|
1330
|
+
assert.notEqual(legacyDialectResult.status, 0);
|
|
1331
|
+
assert.match(output(legacyDialectResult), /REQ-01 is not a canonical requirement id/);
|
|
1332
|
+
|
|
575
1333
|
const phantom = createSpec(root, {
|
|
576
1334
|
name: 'phantom-mapping',
|
|
577
1335
|
task: task({ mapping: '9.1' }),
|
|
@@ -584,7 +1342,8 @@ test('strict validator requires numeric IDs, rejects phantom mappings, and groun
|
|
|
584
1342
|
const researchPlaceholder = createSpec(root, {
|
|
585
1343
|
name: 'research-placeholder',
|
|
586
1344
|
task: task({ related: '`src/strict-only.js` | Read' }),
|
|
587
|
-
|
|
1345
|
+
design: criticalNoObligationDesign(),
|
|
1346
|
+
spec: { workflow_policy: strictPolicy, research: 'research.md' },
|
|
588
1347
|
});
|
|
589
1348
|
write(path.join(researchPlaceholder, 'research.md'), '# Research\n\n## Evidence Summary\n- TBD\n');
|
|
590
1349
|
const placeholderResult = run(VALIDATOR, [researchPlaceholder], ROOT);
|
|
@@ -599,7 +1358,211 @@ test('strict validator requires numeric IDs, rejects phantom mappings, and groun
|
|
|
599
1358
|
}
|
|
600
1359
|
});
|
|
601
1360
|
|
|
602
|
-
test('
|
|
1361
|
+
test('validator rejects unsupported or duplicate structured requirement IDs without mining incidental prose', () => {
|
|
1362
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-requirement-inventory-'));
|
|
1363
|
+
try {
|
|
1364
|
+
const unsupportedHeading = createSpec(root, {
|
|
1365
|
+
name: 'unsupported-heading',
|
|
1366
|
+
requirements: '# Requirements\n\n### R1A: One\n\n- **R1.1** The service shall return status value 1.\n',
|
|
1367
|
+
});
|
|
1368
|
+
let result = run(VALIDATOR, [unsupportedHeading], ROOT);
|
|
1369
|
+
assert.notEqual(result.status, 0);
|
|
1370
|
+
assert.match(output(result), /unsupported requirement id R1A/);
|
|
1371
|
+
|
|
1372
|
+
const unsupportedCriterion = createSpec(root, {
|
|
1373
|
+
name: 'unsupported-criterion',
|
|
1374
|
+
requirements: '# Requirements\n\n### Requirement 1: One\n\n- **R1-1** The service shall return status value 1.\n',
|
|
1375
|
+
});
|
|
1376
|
+
result = run(VALIDATOR, [unsupportedCriterion], ROOT);
|
|
1377
|
+
assert.notEqual(result.status, 0);
|
|
1378
|
+
assert.match(output(result), /unsupported acceptance criterion id R1-1/);
|
|
1379
|
+
|
|
1380
|
+
const duplicates = createSpec(root, {
|
|
1381
|
+
name: 'duplicate-inventory',
|
|
1382
|
+
requirements: '# Requirements\n\n### Requirement 1: First\n\n- **R1.1** The service shall return status value 1.\n\n' +
|
|
1383
|
+
'### R1: Duplicate\n\n- **R1.1** The service shall return status value 2.\n',
|
|
1384
|
+
});
|
|
1385
|
+
result = run(VALIDATOR, [duplicates], ROOT);
|
|
1386
|
+
assert.notEqual(result.status, 0);
|
|
1387
|
+
assert.match(output(result), /duplicate canonical requirement id R1/);
|
|
1388
|
+
assert.match(output(result), /duplicate canonical acceptance criterion id R1\.1/);
|
|
1389
|
+
|
|
1390
|
+
const incidental = createSpec(root, {
|
|
1391
|
+
name: 'incidental-rfc-adr-and-reference',
|
|
1392
|
+
task: task({ related: '`src/one.js` | Read' }),
|
|
1393
|
+
requirements: '# Requirements\n\n### Requirement 1: One\n\n' +
|
|
1394
|
+
'- **R1.1** The service shall return one deterministic result with status value 1.\n\n' +
|
|
1395
|
+
'See RFC 9110, ADR-001, and R7.2 for historical context only.\n',
|
|
1396
|
+
});
|
|
1397
|
+
result = run(VALIDATOR, [incidental], ROOT);
|
|
1398
|
+
assert.equal(result.status, 0, output(result));
|
|
1399
|
+
|
|
1400
|
+
const ignoredSyntax = createSpec(root, {
|
|
1401
|
+
name: 'ignored-fenced-and-commented-inventory',
|
|
1402
|
+
task: task({ related: '`src/one.js` | Read' }),
|
|
1403
|
+
requirements: '# Requirements\n\n### Requirement 1: One\n\n' +
|
|
1404
|
+
'- **R1.1** The service shall return one deterministic result with status value 1.\n\n' +
|
|
1405
|
+
'<!-- ### Requirement 1: commented duplicate\n- **R1.1** commented duplicate -->\n\n' +
|
|
1406
|
+
'```markdown\n### Requirement 1: example duplicate\n- **R1.1** example duplicate\n```\n',
|
|
1407
|
+
});
|
|
1408
|
+
result = run(VALIDATOR, [ignoredSyntax], ROOT);
|
|
1409
|
+
assert.equal(result.status, 0, output(result));
|
|
1410
|
+
|
|
1411
|
+
for (const [name, heading] of [
|
|
1412
|
+
['numeric-heading', '### 1: One'],
|
|
1413
|
+
['bracket-heading', '### [R1]: One'],
|
|
1414
|
+
]) {
|
|
1415
|
+
const invalidHeading = createSpec(root, {
|
|
1416
|
+
name,
|
|
1417
|
+
requirements: `# Requirements\n\n${heading}\n\n- **R1.1** The service shall return status value 1.\n`,
|
|
1418
|
+
});
|
|
1419
|
+
result = run(VALIDATOR, [invalidHeading], ROOT);
|
|
1420
|
+
assert.notEqual(result.status, 0);
|
|
1421
|
+
assert.match(output(result), /unsupported requirement id/);
|
|
1422
|
+
}
|
|
1423
|
+
} finally {
|
|
1424
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
test('validator requires substantive Specs v2 task semantics and planned executable proof', () => {
|
|
1429
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-task-semantics-'));
|
|
1430
|
+
try {
|
|
1431
|
+
const invalid = createSpec(root, { name: 'weak-task-semantics' });
|
|
1432
|
+
const weakTask = task({ related: '`src/one.js` | Read' })
|
|
1433
|
+
.replace('Calling the real fixture entrypoint returns status value 1.', 'TBD')
|
|
1434
|
+
.replace('- **In scope:** Implement the mapped observable behavior.', '- **In scope:** TBD')
|
|
1435
|
+
.replace('- **Out of scope:** Unrelated runtime behavior.', '- **Out of scope:** N/A')
|
|
1436
|
+
.replace('- **R1.1:** Calling `src/one.js` returns an observable result with status value 1.', '- **R1.1:** Works')
|
|
1437
|
+
.replace('| A-R1-01-01 | command | `node --test test/one.test.js` |', '| A-R1-01-01 | command | `npm run fixture-check` |')
|
|
1438
|
+
.replace('- **Command:** `node --test test/one.test.js`', '- **Command:** run tests')
|
|
1439
|
+
.replace('exit code 0 and one assertion for status value 1', 'looks good')
|
|
1440
|
+
.replace('invalid input returns a deterministic error result', 'TBD')
|
|
1441
|
+
.replace('`src/one.js` is invoked by the focused test', 'TBD');
|
|
1442
|
+
writeRaw(path.join(invalid, 'tasks/task-R1-01-one.md'), weakTask);
|
|
1443
|
+
const rejected = run(VALIDATOR, [invalid], ROOT);
|
|
1444
|
+
assert.notEqual(rejected.status, 0);
|
|
1445
|
+
assert.match(output(rejected), /Outcome must be substantive/);
|
|
1446
|
+
assert.match(output(rejected), /Verification Plan requires a command-shaped invocation or justified N\/A/);
|
|
1447
|
+
assert.match(output(rejected), /Verification Plan requires an observable Expected result/);
|
|
1448
|
+
assert.match(output(rejected), /Verification Plan requires a negative-path disposition/);
|
|
1449
|
+
assert.match(output(rejected), /Verification Plan requires a concrete reachability target or justified N\/A/);
|
|
1450
|
+
|
|
1451
|
+
const emptyExecution = createSpec(root, {
|
|
1452
|
+
name: 'empty-steps-mapping-only-requirements',
|
|
1453
|
+
task: task({ related: '`src/empty.js` | Read' })
|
|
1454
|
+
.replace(/## Changes\n[\s\S]*?\n## Acceptance/, '## Changes\n\n## Acceptance'),
|
|
1455
|
+
});
|
|
1456
|
+
const emptyExecutionResult = run(VALIDATOR, [emptyExecution], ROOT);
|
|
1457
|
+
assert.notEqual(emptyExecutionResult.status, 0);
|
|
1458
|
+
assert.match(output(emptyExecutionResult), /Changes must be substantive|Changes must contain at least one substantive actionable checkbox/);
|
|
1459
|
+
|
|
1460
|
+
const justified = createSpec(root, {
|
|
1461
|
+
name: 'justified-command-na',
|
|
1462
|
+
task: task({ related: '`src/static-contract.js` | Read' }).replace(
|
|
1463
|
+
'- **Command:** `node --test test/one.test.js`',
|
|
1464
|
+
'- Command: N/A — no executable runtime exists because this task only inspects a static contract',
|
|
1465
|
+
),
|
|
1466
|
+
});
|
|
1467
|
+
const accepted = run(VALIDATOR, [justified], ROOT);
|
|
1468
|
+
assert.equal(accepted.status, 0, output(accepted));
|
|
1469
|
+
|
|
1470
|
+
const projectSpecific = createSpec(root, {
|
|
1471
|
+
name: 'project-specific-command-shape',
|
|
1472
|
+
task: task({ related: '`src/project-specific.js` | Read' })
|
|
1473
|
+
.replace('node --test test/one.test.js', 'npm run project-specific-check'),
|
|
1474
|
+
});
|
|
1475
|
+
const projectSpecificResult = run(VALIDATOR, [projectSpecific], ROOT);
|
|
1476
|
+
assert.equal(projectSpecificResult.status, 0, output(projectSpecificResult));
|
|
1477
|
+
} finally {
|
|
1478
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
test('all ready non-Direct specs require semantic design and coherent authoring timestamps', () => {
|
|
1483
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-ready-lifecycle-'));
|
|
1484
|
+
|
|
1485
|
+
function readyFixture(name, mutate = () => {}) {
|
|
1486
|
+
const specDir = createSpec(root, {
|
|
1487
|
+
name,
|
|
1488
|
+
task: task({ related: '`src/one.js` | Read' }),
|
|
1489
|
+
spec: { workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }) },
|
|
1490
|
+
});
|
|
1491
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
1492
|
+
const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
1493
|
+
state.ready_for_implementation = true;
|
|
1494
|
+
state.validation = { status: 'completed' };
|
|
1495
|
+
state.updated_at = '2026-08-11T00:06:00+07:00';
|
|
1496
|
+
state.timestamps = lifecycleTimestamps();
|
|
1497
|
+
mutate(state, specDir);
|
|
1498
|
+
write(specPath, JSON.stringify(state, null, 2));
|
|
1499
|
+
completeSemanticReview(
|
|
1500
|
+
specDir,
|
|
1501
|
+
state.workflow_policy.assurance_level === 'Strict' ? 'independent' : 'same-session',
|
|
1502
|
+
);
|
|
1503
|
+
return specDir;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
try {
|
|
1507
|
+
const critical = readyFixture('critical-generic-design', (state, specDir) => {
|
|
1508
|
+
state.workflow_policy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
|
|
1509
|
+
state.timestamps = lifecycleTimestamps({ research: true });
|
|
1510
|
+
write(path.join(specDir, 'design.md'), '# Design\n\n## Architecture\n\nThe implementation boundary is defined and appropriate.\n');
|
|
1511
|
+
});
|
|
1512
|
+
let result = run(VALIDATOR, [critical], ROOT);
|
|
1513
|
+
assert.notEqual(result.status, 0);
|
|
1514
|
+
assert.match(output(result), /design\.md: Critical readiness requires a grounded path\/code identifier/);
|
|
1515
|
+
|
|
1516
|
+
const strictAuthoring = readyFixture('critical-one-task-authoring', (state, specDir) => {
|
|
1517
|
+
state.workflow_policy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
|
|
1518
|
+
state.timestamps = lifecycleTimestamps({ research: true });
|
|
1519
|
+
write(path.join(specDir, 'design.md'), criticalNoObligationDesign());
|
|
1520
|
+
});
|
|
1521
|
+
result = run(VALIDATOR, [strictAuthoring], ROOT);
|
|
1522
|
+
assert.equal(result.status, 0, output(result));
|
|
1523
|
+
|
|
1524
|
+
const invalidCases = [
|
|
1525
|
+
['boolean-created-at', (state) => { state.created_at = true; }, /created_at: must be an ISO 8601 timestamp|created_at: ready handoff requires/],
|
|
1526
|
+
['boolean-task-time', (state) => { state.timestamps.tasks_done = true; }, /timestamps\.tasks_done: must be null or an ISO 8601 timestamp|ready handoff requires/],
|
|
1527
|
+
['missing-validation-time', (state) => { delete state.timestamps.validation_done; }, /timestamps\.validation_done: ready handoff requires/],
|
|
1528
|
+
['reversed-phase-time', (state) => { state.timestamps.design_done = '2026-08-11T00:00:30+07:00'; }, /final lifecycle timestamps must be ordered/],
|
|
1529
|
+
['mismatched-final-state', (state) => { state.updated_at = '2026-08-11T00:07:00+07:00'; }, /updated_at must describe the same final state/],
|
|
1530
|
+
];
|
|
1531
|
+
for (const [name, mutate, expected] of invalidCases) {
|
|
1532
|
+
const specDir = readyFixture(name, mutate);
|
|
1533
|
+
result = run(VALIDATOR, [specDir], ROOT);
|
|
1534
|
+
assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
|
|
1535
|
+
assert.match(output(result), expected);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const reviewDir = readyFixture('five-task-review-time', (state, specDir) => {
|
|
1539
|
+
state.design_context = { validation_recommended: true };
|
|
1540
|
+
for (let index = 2; index <= 5; index += 1) {
|
|
1541
|
+
const taskPath = `tasks/task-R1-0${index}-review.md`;
|
|
1542
|
+
state.task_files.push(taskPath);
|
|
1543
|
+
state.task_registry[taskPath] = {
|
|
1544
|
+
id: `R1-0${index}`, title: `Review ${index}`, status: 'pending', dependencies: [], blocker: null,
|
|
1545
|
+
started_at: null, completed_at: null, last_updated_at: null,
|
|
1546
|
+
};
|
|
1547
|
+
write(path.join(specDir, taskPath), task({ id: `R1-0${index}`, related: `\`src/review-${index}.js\` | Read` }));
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
result = run(VALIDATOR, [reviewDir], ROOT);
|
|
1551
|
+
assert.notEqual(result.status, 0);
|
|
1552
|
+
assert.match(output(result), /timestamps\.review_done: required after validation for a large task graph/);
|
|
1553
|
+
|
|
1554
|
+
const reviewPath = path.join(reviewDir, 'spec.json');
|
|
1555
|
+
const reviewState = JSON.parse(fs.readFileSync(reviewPath, 'utf8'));
|
|
1556
|
+
reviewState.timestamps.review_done = '2026-08-11T00:05:00+07:00';
|
|
1557
|
+
write(reviewPath, JSON.stringify(reviewState, null, 2));
|
|
1558
|
+
result = run(VALIDATOR, [reviewDir], ROOT);
|
|
1559
|
+
assert.equal(result.status, 0, output(result));
|
|
1560
|
+
} finally {
|
|
1561
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
test('spec-ready excludes execution receipts while preserving Strict review state', () => {
|
|
603
1566
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-spec-ready-'));
|
|
604
1567
|
try {
|
|
605
1568
|
const standardDir = createSpec(root, {
|
|
@@ -614,19 +1577,20 @@ test('spec-ready accepts pending execution evidence without an audit receipt', (
|
|
|
614
1577
|
tasks: { generated: true, agent_validated: true, user_approved: true },
|
|
615
1578
|
},
|
|
616
1579
|
validation: { status: 'completed' },
|
|
617
|
-
|
|
1580
|
+
updated_at: '2026-08-11T00:06:00+07:00',
|
|
1581
|
+
timestamps: lifecycleTimestamps(),
|
|
618
1582
|
},
|
|
619
1583
|
});
|
|
620
|
-
|
|
621
|
-
write(path.join(standardDir, 'tasks/task-R1-01-one.md'), `${fs.readFileSync(path.join(standardDir, 'tasks/task-R1-01-one.md'), 'utf8')}\nVerification: PENDING\n`);
|
|
1584
|
+
completeSemanticReview(standardDir);
|
|
622
1585
|
const standardResult = run(VALIDATOR, [standardDir], ROOT);
|
|
623
1586
|
assert.equal(standardResult.status, 0, output(standardResult));
|
|
624
1587
|
|
|
625
1588
|
const criticalDir = createSpec(root, {
|
|
626
1589
|
name: 'pending-critical',
|
|
627
1590
|
task: task({ related: '`src/pending-only.js` | Read' }),
|
|
1591
|
+
design: criticalNoObligationDesign(),
|
|
628
1592
|
spec: {
|
|
629
|
-
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } }),
|
|
1593
|
+
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
|
|
630
1594
|
ready_for_implementation: true,
|
|
631
1595
|
approvals: {
|
|
632
1596
|
requirements: { generated: true, agent_validated: true, user_approved: true },
|
|
@@ -634,9 +1598,11 @@ test('spec-ready accepts pending execution evidence without an audit receipt', (
|
|
|
634
1598
|
tasks: { generated: true, agent_validated: true, user_approved: true },
|
|
635
1599
|
},
|
|
636
1600
|
validation: { status: 'completed' },
|
|
637
|
-
|
|
1601
|
+
updated_at: '2026-08-11T00:06:00+07:00',
|
|
1602
|
+
timestamps: lifecycleTimestamps({ research: true, review: true }),
|
|
638
1603
|
},
|
|
639
1604
|
});
|
|
1605
|
+
completeSemanticReview(criticalDir, 'independent');
|
|
640
1606
|
const criticalResult = run(VALIDATOR, [criticalDir], ROOT);
|
|
641
1607
|
assert.equal(criticalResult.status, 0, output(criticalResult));
|
|
642
1608
|
} finally {
|
|
@@ -644,7 +1610,7 @@ test('spec-ready accepts pending execution evidence without an audit receipt', (
|
|
|
644
1610
|
}
|
|
645
1611
|
});
|
|
646
1612
|
|
|
647
|
-
test('
|
|
1613
|
+
test('Specs v2 Compact validates without task registry or premature receipt and fails closed on policy shape errors', () => {
|
|
648
1614
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-bounded-'));
|
|
649
1615
|
try {
|
|
650
1616
|
const specDir = createBoundedSpec(root);
|
|
@@ -655,12 +1621,11 @@ test('P1 bounded Standard validates without a task registry and fails closed on
|
|
|
655
1621
|
assert.equal(Object.hasOwn(persisted, 'task_files'), false);
|
|
656
1622
|
assert.equal(Object.hasOwn(persisted, 'task_registry'), false);
|
|
657
1623
|
|
|
658
|
-
fs.unlinkSync(path.join(specDir, 'feature-receipt.md'));
|
|
659
|
-
const missingReceipt = run(VALIDATOR, [specDir], ROOT);
|
|
660
|
-
assert.notEqual(missingReceipt.status, 0);
|
|
661
|
-
assert.match(output(missingReceipt), /feature-receipt\.md: required/);
|
|
662
|
-
|
|
663
1624
|
write(path.join(specDir, 'feature-receipt.md'), '# Feature Verification Receipt\n\nVerification: PENDING\n\nStatus: in_progress\n\nBlocker: awaiting execution proof\n');
|
|
1625
|
+
const prematureReceipt = run(VALIDATOR, [specDir], ROOT);
|
|
1626
|
+
assert.notEqual(prematureReceipt.status, 0);
|
|
1627
|
+
assert.match(output(prematureReceipt), /premature execution receipt/);
|
|
1628
|
+
fs.unlinkSync(path.join(specDir, 'feature-receipt.md'));
|
|
664
1629
|
const malformed = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
665
1630
|
malformed.workflow_policy.extra = true;
|
|
666
1631
|
fs.writeFileSync(path.join(specDir, 'spec.json'), JSON.stringify(malformed, null, 2));
|
|
@@ -672,7 +1637,7 @@ test('P1 bounded Standard validates without a task registry and fails closed on
|
|
|
672
1637
|
}
|
|
673
1638
|
});
|
|
674
1639
|
|
|
675
|
-
test('
|
|
1640
|
+
test('Specs v2 scaffold keeps planning artifacts independent from assurance and task topology', () => {
|
|
676
1641
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-lanes-'));
|
|
677
1642
|
try {
|
|
678
1643
|
const scaffoldSource = fs.readFileSync(SCAFFOLD, 'utf8');
|
|
@@ -687,75 +1652,89 @@ test('P1 Standard scaffold remains bounded while Critical risk requires a task b
|
|
|
687
1652
|
assert.equal(standard.status, 0, output(standard));
|
|
688
1653
|
const standardDir = path.join(root, 'specs', 'bounded-standard');
|
|
689
1654
|
const standardSpec = JSON.parse(fs.readFileSync(path.join(standardDir, 'spec.json'), 'utf8'));
|
|
690
|
-
assert.equal(standardSpec.
|
|
691
|
-
assert.deepEqual(standardSpec.
|
|
1655
|
+
assert.equal(POLICY.readWorkflowPolicySnapshot(standardSpec).lane, 'Standard');
|
|
1656
|
+
assert.deepEqual(POLICY.readWorkflowPolicySnapshot(standardSpec).proof_obligations, ['needsExecutionProof']);
|
|
692
1657
|
assert.equal(fs.existsSync(path.join(standardDir, 'tasks')), false);
|
|
693
1658
|
assert.equal(fs.existsSync(path.join(standardDir, 'task_registry')), false);
|
|
1659
|
+
assert.equal(standardSpec.validation.semantic_review.status, 'not-run');
|
|
1660
|
+
assert.equal(fs.existsSync(path.join(standardDir, 'semantic-review.md')), false);
|
|
1661
|
+
write(path.join(standardDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** The scaffolded command shall return status value 1.\n- **R1.2** The proof shall reject an invalid status result.\n');
|
|
1662
|
+
write(path.join(standardDir, 'design.md'), designFixture21());
|
|
1663
|
+
const standardPromoted = run(SCAFFOLD, ['bounded-standard', '--sync-semantic-model'], root);
|
|
1664
|
+
assert.equal(standardPromoted.status, 0, output(standardPromoted));
|
|
694
1665
|
const standardValidation = run(VALIDATOR, [standardDir], ROOT);
|
|
695
1666
|
assert.equal(standardValidation.status, 0, output(standardValidation));
|
|
696
1667
|
|
|
697
|
-
const
|
|
698
|
-
assert.equal(
|
|
699
|
-
|
|
1668
|
+
const elevated = run(SCAFFOLD, ['elevated-risk', '--risks', 'auth'], root);
|
|
1669
|
+
assert.equal(elevated.status, 0, output(elevated));
|
|
1670
|
+
const elevatedTaskless = JSON.parse(fs.readFileSync(path.join(root, 'specs/elevated-risk/spec.json'), 'utf8'));
|
|
1671
|
+
assert.equal(elevatedTaskless.workflow_policy.planning_depth, 'Compact');
|
|
1672
|
+
assert.equal(elevatedTaskless.workflow_policy.assurance_level, 'Elevated');
|
|
1673
|
+
assert.equal(elevatedTaskless.workflow_policy.classified_minimum.assurance_level, 'Elevated');
|
|
1674
|
+
assert.deepEqual(elevatedTaskless.coordination, { boundaries: [] });
|
|
1675
|
+
assert.equal(fs.existsSync(path.join(root, 'specs/elevated-risk/research.md')), false);
|
|
1676
|
+
assert.equal(elevatedTaskless.validation.semantic_review.status, 'not-run');
|
|
1677
|
+
assert.equal(elevatedTaskless.authoring.tasks, 'absent');
|
|
1678
|
+
|
|
1679
|
+
const strict = run(SCAFFOLD, ['strict-opt-in', '--risks', 'auth', '--assurance-level', 'Strict'], root);
|
|
1680
|
+
assert.equal(strict.status, 0, output(strict));
|
|
1681
|
+
const strictTaskless = JSON.parse(fs.readFileSync(path.join(root, 'specs/strict-opt-in/spec.json'), 'utf8'));
|
|
1682
|
+
assert.equal(strictTaskless.workflow_policy.assurance_level, 'Strict');
|
|
1683
|
+
assert.equal(strictTaskless.workflow_policy.classified_minimum.assurance_level, 'Elevated');
|
|
1684
|
+
assert.equal(strictTaskless.authoring.tasks, 'absent');
|
|
1685
|
+
} finally {
|
|
1686
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
700
1689
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1690
|
+
test('Full phase scaffold fills canonical 2.1 artifacts and passes E2E validation', () => {
|
|
1691
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-full-phase-'));
|
|
1692
|
+
try {
|
|
1693
|
+
const phase = [{
|
|
1694
|
+
id: 'delivery',
|
|
1695
|
+
task_ids: ['R1-01', 'R1-02'],
|
|
1696
|
+
entry_condition: 'requirements and design are approved',
|
|
1697
|
+
exit_condition: 'task verification plan is complete',
|
|
1698
|
+
owner_boundary: 'B-T',
|
|
1699
|
+
}];
|
|
1700
|
+
const result = run(SCAFFOLD, [
|
|
1701
|
+
'full-phase', '--planning-depth', 'Full', '--tasks', 'R1-01-artifact,R1-02-consumer',
|
|
1702
|
+
'--boundaries', JSON.stringify([{
|
|
1703
|
+
id: 'B-T', type: 'transition', design_ref: 'T1', owner: 'R1-01', consumers: ['R1-02'],
|
|
1704
|
+
precondition: 'Input is ready for transition', postcondition: 'Output is durably complete',
|
|
1705
|
+
failure: 'Failure state is recorded', recovery: 'Recovery resumes from recorded state',
|
|
1706
|
+
}]), '--phases', JSON.stringify(phase),
|
|
705
1707
|
], root);
|
|
706
|
-
assert.equal(
|
|
707
|
-
const
|
|
708
|
-
const
|
|
709
|
-
assert.
|
|
710
|
-
assert.equal(
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
'needsIndependentAudit',
|
|
715
|
-
'needsResearchGrounding',
|
|
716
|
-
]);
|
|
717
|
-
assert.deepEqual(criticalSpec.task_files, ['tasks/task-R1-01-artifact.md']);
|
|
718
|
-
assert.equal(fs.existsSync(path.join(criticalDir, 'research.md')), true);
|
|
719
|
-
completeScaffoldedSpec(criticalDir);
|
|
720
|
-
const criticalValidation = run(VALIDATOR, [criticalDir], ROOT);
|
|
721
|
-
assert.equal(criticalValidation.status, 0, output(criticalValidation));
|
|
1708
|
+
assert.equal(result.status, 0, output(result));
|
|
1709
|
+
const specDir = path.join(root, 'specs/full-phase');
|
|
1710
|
+
const spec = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
1711
|
+
assert.deepEqual(spec.coordination.phases, phase);
|
|
1712
|
+
assert.equal(spec.coordination.boundaries[0].type, 'transition');
|
|
1713
|
+
completeScaffoldedSpec21(specDir);
|
|
1714
|
+
const validation = run(VALIDATOR, [specDir], ROOT);
|
|
1715
|
+
assert.equal(validation.status, 0, output(validation));
|
|
722
1716
|
} finally {
|
|
723
1717
|
fs.rmSync(root, { recursive: true, force: true });
|
|
724
1718
|
}
|
|
725
1719
|
});
|
|
726
1720
|
|
|
727
|
-
test('producer scaffold
|
|
1721
|
+
test('producer scaffold projects typed dependency deliverable into registry edges', () => {
|
|
728
1722
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-artifacts-'));
|
|
729
1723
|
try {
|
|
730
|
-
const ordinary = run(SCAFFOLD, [
|
|
1724
|
+
const ordinary = run(SCAFFOLD, [
|
|
1725
|
+
'ordinary', '--tasks', 'R1-01-artifact,R1-02-consumer',
|
|
1726
|
+
'--boundaries', JSON.stringify([{
|
|
1727
|
+
id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
|
|
1728
|
+
deliverable: 'artifacts/output.json',
|
|
1729
|
+
}]),
|
|
1730
|
+
], root);
|
|
731
1731
|
assert.equal(ordinary.status, 0, output(ordinary));
|
|
732
1732
|
const ordinarySpec = JSON.parse(fs.readFileSync(path.join(root, 'specs/ordinary/spec.json'), 'utf8'));
|
|
733
1733
|
assert.equal(Object.hasOwn(ordinarySpec.task_registry['tasks/task-R1-01-artifact.md'], 'artifacts'), false);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const declared = run(SCAFFOLD, [
|
|
739
|
-
'declared',
|
|
740
|
-
'--tasks',
|
|
741
|
-
'R1-01-artifact',
|
|
742
|
-
'--artifacts',
|
|
743
|
-
JSON.stringify({ 'R1-01-artifact': ['dist/output.js'] }),
|
|
744
|
-
], root);
|
|
745
|
-
assert.equal(declared.status, 0, output(declared));
|
|
746
|
-
const declaredDir = path.join(root, 'specs/declared');
|
|
747
|
-
const declaredSpec = completeScaffoldedSpec(declaredDir);
|
|
748
|
-
assert.deepEqual(declaredSpec.task_registry['tasks/task-R1-01-artifact.md'].artifacts, ['dist/output.js']);
|
|
749
|
-
const declaredValidation = run(VALIDATOR, [declaredDir], ROOT);
|
|
750
|
-
assert.equal(declaredValidation.status, 0, output(declaredValidation));
|
|
751
|
-
|
|
752
|
-
const malformedPath = path.join(declaredDir, 'spec.json');
|
|
753
|
-
const malformedSpec = JSON.parse(fs.readFileSync(malformedPath, 'utf8'));
|
|
754
|
-
malformedSpec.task_registry['tasks/task-R1-01-artifact.md'].artifacts = ['../outside'];
|
|
755
|
-
fs.writeFileSync(malformedPath, JSON.stringify(malformedSpec, null, 2));
|
|
756
|
-
const malformedValidation = run(VALIDATOR, [declaredDir], ROOT);
|
|
757
|
-
assert.notEqual(malformedValidation.status, 0);
|
|
758
|
-
assert.match(output(malformedValidation), /task_registry.*artifacts.*safe relative path/);
|
|
1734
|
+
assert.deepEqual(ordinarySpec.task_registry['tasks/task-R1-02-consumer.md'].dependencies, [
|
|
1735
|
+
'tasks/task-R1-01-artifact.md',
|
|
1736
|
+
]);
|
|
1737
|
+
assert.equal(ordinarySpec.coordination.boundaries[0].deliverable, 'artifacts/output.json');
|
|
759
1738
|
} finally {
|
|
760
1739
|
fs.rmSync(root, { recursive: true, force: true });
|
|
761
1740
|
}
|
|
@@ -784,46 +1763,84 @@ test('validator rejects missing explicit requirement and sub-criterion mappings'
|
|
|
784
1763
|
const partialResult = run(VALIDATOR, [partial], ROOT);
|
|
785
1764
|
assert.notEqual(partialResult.status, 0);
|
|
786
1765
|
assert.match(output(partialResult), /R1\.2: acceptance criterion not covered/);
|
|
1766
|
+
|
|
1767
|
+
const standardUnknown = createSpec(root, {
|
|
1768
|
+
name: 'standard-unknown-mapping',
|
|
1769
|
+
task: task({ mapping: '1.1, 9.1' }),
|
|
1770
|
+
spec: { workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }) },
|
|
1771
|
+
});
|
|
1772
|
+
const standardUnknownResult = run(VALIDATOR, [standardUnknown], ROOT);
|
|
1773
|
+
assert.notEqual(standardUnknownResult.status, 0);
|
|
1774
|
+
assert.match(output(standardUnknownResult), /references unknown requirement R9/);
|
|
787
1775
|
} finally {
|
|
788
1776
|
fs.rmSync(root, { recursive: true, force: true });
|
|
789
1777
|
}
|
|
790
1778
|
});
|
|
791
1779
|
|
|
792
|
-
test('
|
|
1780
|
+
test('v1 adapter rejects unknown, missing, and divergent contract copies', () => {
|
|
793
1781
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-contract-'));
|
|
794
|
-
const design =
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
name
|
|
798
|
-
|
|
1782
|
+
const design = `${designFixture()}<!-- contract:PAYLOAD -->\n\`\`\`json\n{ "id": 1 }\n\`\`\`\n`;
|
|
1783
|
+
function contractFixture(name, { designText = design, extra = '' } = {}) {
|
|
1784
|
+
return createSpec(root, {
|
|
1785
|
+
name,
|
|
1786
|
+
design: designText,
|
|
1787
|
+
task: legacyTask({ extra }),
|
|
1788
|
+
spec: { workflow_policy: legacyWorkflowPolicy() },
|
|
799
1789
|
});
|
|
1790
|
+
}
|
|
1791
|
+
try {
|
|
1792
|
+
const unknown = contractFixture('unknown', { extra: 'Contracts: MISSING\n' });
|
|
800
1793
|
let result = run(VALIDATOR, [unknown], ROOT);
|
|
801
1794
|
assert.notEqual(result.status, 0);
|
|
802
1795
|
assert.match(output(result), /unknown contract "MISSING"/);
|
|
803
1796
|
|
|
804
|
-
const noCanonical =
|
|
805
|
-
|
|
806
|
-
|
|
1797
|
+
const noCanonical = contractFixture('no-canonical', {
|
|
1798
|
+
designText: designFixture(),
|
|
1799
|
+
extra: 'Contracts: PAYLOAD\n',
|
|
807
1800
|
});
|
|
808
1801
|
result = run(VALIDATOR, [noCanonical], ROOT);
|
|
809
1802
|
assert.notEqual(result.status, 0);
|
|
810
1803
|
assert.match(output(result), /defines no canonical contract blocks/);
|
|
811
1804
|
|
|
812
|
-
const missing =
|
|
813
|
-
name: 'missing', design,
|
|
814
|
-
task: task({ extra: 'Contracts: PAYLOAD\n' }),
|
|
815
|
-
});
|
|
1805
|
+
const missing = contractFixture('missing', { extra: 'Contracts: PAYLOAD\n' });
|
|
816
1806
|
result = run(VALIDATOR, [missing], ROOT);
|
|
817
1807
|
assert.notEqual(result.status, 0);
|
|
818
1808
|
assert.match(output(result), /contract "PAYLOAD" is missing/);
|
|
819
1809
|
|
|
820
|
-
const divergent =
|
|
821
|
-
|
|
822
|
-
task: task({ extra: 'Contracts: PAYLOAD\n\n```json\n{ "id": 2 }\n```\n' }),
|
|
1810
|
+
const divergent = contractFixture('divergent', {
|
|
1811
|
+
extra: 'Contracts: PAYLOAD\n\n```json\n{ "id": 2 }\n```\n',
|
|
823
1812
|
});
|
|
824
1813
|
result = run(VALIDATOR, [divergent], ROOT);
|
|
825
1814
|
assert.notEqual(result.status, 0);
|
|
826
1815
|
assert.match(output(result), /contract "PAYLOAD" body diverges/);
|
|
1816
|
+
|
|
1817
|
+
const duplicateDesign = contractFixture('duplicate-design-contract', {
|
|
1818
|
+
designText: `${design}\n<!-- contract:PAYLOAD -->\n\`\`\`json\n{ "id": 1 }\n\`\`\`\n`,
|
|
1819
|
+
});
|
|
1820
|
+
result = run(VALIDATOR, [duplicateDesign], ROOT);
|
|
1821
|
+
assert.notEqual(result.status, 0);
|
|
1822
|
+
assert.match(output(result), /duplicate canonical contract definition "PAYLOAD"/);
|
|
1823
|
+
|
|
1824
|
+
const repeatedDeclaration = contractFixture('repeated-task-contract-line', {
|
|
1825
|
+
extra: 'Contracts: PAYLOAD\nContracts: PAYLOAD\n\n```json\n{ "id": 1 }\n```\n',
|
|
1826
|
+
});
|
|
1827
|
+
result = run(VALIDATOR, [repeatedDeclaration], ROOT);
|
|
1828
|
+
assert.notEqual(result.status, 0);
|
|
1829
|
+
assert.match(output(result), /Contracts declaration must appear at most once/);
|
|
1830
|
+
|
|
1831
|
+
const duplicateName = contractFixture('duplicate-task-contract-name', {
|
|
1832
|
+
extra: 'Contracts: PAYLOAD, PAYLOAD\n\n```json\n{ "id": 1 }\n```\n',
|
|
1833
|
+
});
|
|
1834
|
+
result = run(VALIDATOR, [duplicateName], ROOT);
|
|
1835
|
+
assert.notEqual(result.status, 0);
|
|
1836
|
+
assert.match(output(result), /Contracts declaration contains duplicate name "PAYLOAD"/);
|
|
1837
|
+
|
|
1838
|
+
const orphanTagged = contractFixture('orphan-tagged-contract', {
|
|
1839
|
+
extra: '<!-- contract:PAYLOAD -->\n```json\n{ "id": 1 }\n```\n',
|
|
1840
|
+
});
|
|
1841
|
+
result = run(VALIDATOR, [orphanTagged], ROOT);
|
|
1842
|
+
assert.notEqual(result.status, 0);
|
|
1843
|
+
assert.match(output(result), /must declare it on one Contracts: line/);
|
|
827
1844
|
} finally {
|
|
828
1845
|
fs.rmSync(root, { recursive: true, force: true });
|
|
829
1846
|
}
|
|
@@ -841,8 +1858,8 @@ test('validator rejects invalid ready approval and validation transitions', () =
|
|
|
841
1858
|
});
|
|
842
1859
|
const result = run(VALIDATOR, [specDir], ROOT);
|
|
843
1860
|
assert.notEqual(result.status, 0);
|
|
844
|
-
assert.match(output(result), /requires generated and agent_validated
|
|
845
|
-
assert.match(output(result), /requires generated and agent_validated
|
|
1861
|
+
assert.match(output(result), /requires generated and agent_validated requirements evidence/);
|
|
1862
|
+
assert.match(output(result), /requires generated and agent_validated design evidence/);
|
|
846
1863
|
assert.match(output(result), /validation evidence is incomplete/);
|
|
847
1864
|
|
|
848
1865
|
// Legacy approved field must fail closed with migration guidance
|
|
@@ -875,6 +1892,37 @@ test('validator rejects invalid ready approval and validation transitions', () =
|
|
|
875
1892
|
}
|
|
876
1893
|
});
|
|
877
1894
|
|
|
1895
|
+
test('validator rejects precise square-bracket placeholders without flagging links or checkboxes', () => {
|
|
1896
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-square-placeholders-'));
|
|
1897
|
+
try {
|
|
1898
|
+
for (const [name, placeholder] of [
|
|
1899
|
+
['specific-value', '[specific value]'],
|
|
1900
|
+
['target-users', '[target users]'],
|
|
1901
|
+
['component-name', '[Component Name]'],
|
|
1902
|
+
]) {
|
|
1903
|
+
const specDir = createSpec(root, {
|
|
1904
|
+
name,
|
|
1905
|
+
task: task({ related: '`src/placeholder.js` | Read' }).replace(
|
|
1906
|
+
'Implement the mapped observable behavior.',
|
|
1907
|
+
`Implement the mapped ${placeholder} behavior.`,
|
|
1908
|
+
),
|
|
1909
|
+
});
|
|
1910
|
+
const result = run(VALIDATOR, [specDir], ROOT);
|
|
1911
|
+
assert.notEqual(result.status, 0);
|
|
1912
|
+
assert.match(output(result), /unresolved square-bracket placeholder/);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const safe = createSpec(root, {
|
|
1916
|
+
name: 'safe-link-checkbox',
|
|
1917
|
+
task: `${task({ related: '`src/safe.js` | Read' })}\n[Component Name](https://example.invalid)\n- [ ] Keep this real checkbox.\n`,
|
|
1918
|
+
});
|
|
1919
|
+
const safeResult = run(VALIDATOR, [safe], ROOT);
|
|
1920
|
+
assert.equal(safeResult.status, 0, output(safeResult));
|
|
1921
|
+
} finally {
|
|
1922
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
|
|
878
1926
|
test('validator rejects malformed Related Files and dependency topology', () => {
|
|
879
1927
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-shape-'));
|
|
880
1928
|
try {
|
|
@@ -886,7 +1934,8 @@ test('validator rejects malformed Related Files and dependency topology', () =>
|
|
|
886
1934
|
]) {
|
|
887
1935
|
const specDir = createSpec(root, {
|
|
888
1936
|
name,
|
|
889
|
-
task: related ?
|
|
1937
|
+
task: legacyTask({ relatedRows: related ? `| ${related} | Invalid fixture row |` : '' }),
|
|
1938
|
+
spec: { workflow_policy: legacyWorkflowPolicy() },
|
|
890
1939
|
});
|
|
891
1940
|
const result = run(VALIDATOR, [specDir], ROOT);
|
|
892
1941
|
assert.notEqual(result.status, 0);
|
|
@@ -896,10 +1945,19 @@ test('validator rejects malformed Related Files and dependency topology', () =>
|
|
|
896
1945
|
const cycle = createSpec(root, { name: 'cycle' });
|
|
897
1946
|
const statePath = path.join(cycle, 'spec.json');
|
|
898
1947
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
1948
|
+
state.coordination.task_triggers = ['distinct_ownership', 'real_dependency'];
|
|
899
1949
|
state.task_files.push('tasks/task-R1-02-two.md');
|
|
900
1950
|
state.task_registry['tasks/task-R1-01-one.md'].dependencies = ['tasks/task-R1-02-two.md'];
|
|
901
1951
|
state.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: ['tasks/task-R1-01-one.md'], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
|
|
902
|
-
write(path.join(cycle, 'tasks/task-R1-
|
|
1952
|
+
write(path.join(cycle, 'tasks/task-R1-01-one.md'), task({
|
|
1953
|
+
related: '`src/one.js` | Read',
|
|
1954
|
+
dependencies: 'tasks/task-R1-02-two.md',
|
|
1955
|
+
}));
|
|
1956
|
+
write(path.join(cycle, 'tasks/task-R1-02-two.md'), task({
|
|
1957
|
+
id: 'R1-02',
|
|
1958
|
+
related: '`src/two.js` | Read',
|
|
1959
|
+
dependencies: 'tasks/task-R1-01-one.md',
|
|
1960
|
+
}));
|
|
903
1961
|
write(statePath, JSON.stringify(state, null, 2));
|
|
904
1962
|
const cycleResult = run(VALIDATOR, [cycle], ROOT);
|
|
905
1963
|
assert.notEqual(cycleResult.status, 0);
|
|
@@ -913,7 +1971,11 @@ test('validator rejects malformed Related Files and dependency topology', () =>
|
|
|
913
1971
|
const orderingState = JSON.parse(fs.readFileSync(orderingStatePath, 'utf8'));
|
|
914
1972
|
orderingState.task_files.push('tasks/task-R1-02-two.md');
|
|
915
1973
|
orderingState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
|
|
916
|
-
write(path.join(ordering, 'tasks/task-R1-02-two.md'), task({
|
|
1974
|
+
write(path.join(ordering, 'tasks/task-R1-02-two.md'), task({
|
|
1975
|
+
id: 'R1-02',
|
|
1976
|
+
related: '`src/generated.js` | Modify',
|
|
1977
|
+
relatedRows: '| `src/generated.js` | Modify | Consume generated implementation |',
|
|
1978
|
+
}));
|
|
917
1979
|
write(orderingStatePath, JSON.stringify(orderingState, null, 2));
|
|
918
1980
|
const orderingResult = run(VALIDATOR, [ordering], ROOT);
|
|
919
1981
|
assert.notEqual(orderingResult.status, 0);
|
|
@@ -923,6 +1985,608 @@ test('validator rejects malformed Related Files and dependency topology', () =>
|
|
|
923
1985
|
}
|
|
924
1986
|
});
|
|
925
1987
|
|
|
1988
|
+
test('scaffold validates one dependency map before writes and persists canonical registry edges', () => {
|
|
1989
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-dependencies-'));
|
|
1990
|
+
const taskList = 'R1-01-owner,R1-02-consumer';
|
|
1991
|
+
const dependencyMap = JSON.stringify([{
|
|
1992
|
+
id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
|
|
1993
|
+
deliverable: 'artifacts/owner-output.json',
|
|
1994
|
+
}]);
|
|
1995
|
+
try {
|
|
1996
|
+
const standard = run(SCAFFOLD, [
|
|
1997
|
+
'dependency-positive', '--lane', 'Standard', '--tasks', taskList,
|
|
1998
|
+
'--boundaries', dependencyMap,
|
|
1999
|
+
], root);
|
|
2000
|
+
assert.equal(standard.status, 0, output(standard));
|
|
2001
|
+
const standardDir = path.join(root, 'specs/dependency-positive');
|
|
2002
|
+
const standardSpec = JSON.parse(fs.readFileSync(path.join(standardDir, 'spec.json'), 'utf8'));
|
|
2003
|
+
assert.deepEqual(
|
|
2004
|
+
standardSpec.task_registry['tasks/task-R1-02-consumer.md'].dependencies,
|
|
2005
|
+
['tasks/task-R1-01-owner.md'],
|
|
2006
|
+
);
|
|
2007
|
+
assert.deepEqual(standardSpec.coordination.boundaries, JSON.parse(dependencyMap));
|
|
2008
|
+
const standardTask = fs.readFileSync(
|
|
2009
|
+
path.join(standardDir, 'tasks/task-R1-02-consumer.md'),
|
|
2010
|
+
'utf8',
|
|
2011
|
+
);
|
|
2012
|
+
assert.doesNotMatch(standardTask, /^## Execution Closure/m);
|
|
2013
|
+
assert.doesNotMatch(
|
|
2014
|
+
fs.readFileSync(path.join(standardDir, 'design.md'), 'utf8'),
|
|
2015
|
+
/^## Execution Closure/m,
|
|
2016
|
+
);
|
|
2017
|
+
|
|
2018
|
+
const invalid = run(SCAFFOLD, [
|
|
2019
|
+
'dependency-invalid', '--tasks', taskList, '--boundaries', JSON.stringify([{
|
|
2020
|
+
id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
|
|
2021
|
+
}]),
|
|
2022
|
+
], root);
|
|
2023
|
+
assert.equal(invalid.status, 2, output(invalid));
|
|
2024
|
+
assert.match(output(invalid), /fields must be exactly.*deliverable|dependency fields must be exactly/);
|
|
2025
|
+
assert.equal(fs.existsSync(path.join(root, 'specs/dependency-invalid')), false);
|
|
2026
|
+
|
|
2027
|
+
} finally {
|
|
2028
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
test('task-level (P) requires dependency-free and non-overlapping task ownership', () => {
|
|
2033
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-parallel-'));
|
|
2034
|
+
const ownerPath = 'tasks/task-R1-01-one.md';
|
|
2035
|
+
const siblingPath = 'tasks/task-R1-02-two.md';
|
|
2036
|
+
|
|
2037
|
+
function parallelFixture(name, { dependency = false, overlap = false } = {}) {
|
|
2038
|
+
const ownerTask = task({
|
|
2039
|
+
related: overlap ? '`src/shared.js` | Read' : '`src/parallel-a.js` | Read',
|
|
2040
|
+
relatedRows: overlap
|
|
2041
|
+
? '| `src/shared.js` | Read | Inspect existing |\n| `src/shared.js` | Modify | Update owned behavior |'
|
|
2042
|
+
: null,
|
|
2043
|
+
parallel: true,
|
|
2044
|
+
});
|
|
2045
|
+
const specDir = createSpec(root, { name, task: ownerTask });
|
|
2046
|
+
const statePath = path.join(specDir, 'spec.json');
|
|
2047
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
2048
|
+
state.coordination.task_triggers = dependency
|
|
2049
|
+
? ['parallel_coordination', 'real_dependency', 'separate_proof']
|
|
2050
|
+
: ['parallel_coordination', 'separate_proof'];
|
|
2051
|
+
state.task_files.push(siblingPath);
|
|
2052
|
+
state.task_registry[siblingPath] = {
|
|
2053
|
+
id: 'R1-02', title: 'Two', status: 'pending',
|
|
2054
|
+
dependencies: dependency ? [ownerPath] : [],
|
|
2055
|
+
blocker: null, started_at: null, completed_at: null, last_updated_at: null,
|
|
2056
|
+
};
|
|
2057
|
+
const sibling = task({
|
|
2058
|
+
id: 'R1-02',
|
|
2059
|
+
related: overlap ? '`src/shared.js` | Read' : '`src/parallel-b.js` | Read',
|
|
2060
|
+
dependencies: dependency ? ownerPath : 'none',
|
|
2061
|
+
parallel: dependency,
|
|
2062
|
+
});
|
|
2063
|
+
write(path.join(specDir, siblingPath), sibling);
|
|
2064
|
+
write(statePath, JSON.stringify(state, null, 2));
|
|
2065
|
+
return specDir;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
try {
|
|
2069
|
+
const valid = parallelFixture('parallel-valid');
|
|
2070
|
+
assert.equal(run(VALIDATOR, [valid], ROOT).status, 0);
|
|
2071
|
+
|
|
2072
|
+
const dependency = parallelFixture('parallel-dependency', { dependency: true });
|
|
2073
|
+
const dependencyResult = run(VALIDATOR, [dependency], ROOT);
|
|
2074
|
+
assert.notEqual(dependencyResult.status, 0);
|
|
2075
|
+
assert.match(output(dependencyResult), /task-level \(P\) cannot declare sibling dependencies/);
|
|
2076
|
+
|
|
2077
|
+
const overlap = parallelFixture('parallel-overlap', { overlap: true });
|
|
2078
|
+
const overlapResult = run(VALIDATOR, [overlap], ROOT);
|
|
2079
|
+
assert.notEqual(overlapResult.status, 0);
|
|
2080
|
+
assert.match(output(overlapResult), /task-level \(P\) has Related Files contention on src\/shared\.js/);
|
|
2081
|
+
} finally {
|
|
2082
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
test('validator rejects dependency drift and non-canonical readiness statuses', () => {
|
|
2087
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-readiness-'));
|
|
2088
|
+
const ownerPath = 'tasks/task-R1-01-one.md';
|
|
2089
|
+
const consumerPath = 'tasks/task-R1-02-two.md';
|
|
2090
|
+
try {
|
|
2091
|
+
const driftDir = createSpec(root, {
|
|
2092
|
+
name: 'dependency-aligned',
|
|
2093
|
+
task: task({ related: '`src/owner.js` | Read' }),
|
|
2094
|
+
});
|
|
2095
|
+
const driftStatePath = path.join(driftDir, 'spec.json');
|
|
2096
|
+
const driftState = JSON.parse(fs.readFileSync(driftStatePath, 'utf8'));
|
|
2097
|
+
driftState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
|
|
2098
|
+
driftState.task_files.push(consumerPath);
|
|
2099
|
+
driftState.task_registry[consumerPath] = {
|
|
2100
|
+
id: 'R1-02', title: 'Two', status: 'pending', dependencies: [ownerPath], blocker: null,
|
|
2101
|
+
started_at: null, completed_at: null, last_updated_at: null,
|
|
2102
|
+
};
|
|
2103
|
+
write(
|
|
2104
|
+
path.join(driftDir, consumerPath),
|
|
2105
|
+
task({
|
|
2106
|
+
id: 'R1-02',
|
|
2107
|
+
related: '`src/consumer.js` | Read',
|
|
2108
|
+
dependencies: ownerPath,
|
|
2109
|
+
}),
|
|
2110
|
+
);
|
|
2111
|
+
write(driftStatePath, JSON.stringify(driftState, null, 2));
|
|
2112
|
+
const aligned = run(VALIDATOR, [driftDir], ROOT);
|
|
2113
|
+
assert.equal(aligned.status, 0, output(aligned));
|
|
2114
|
+
|
|
2115
|
+
const consumerFile = path.join(driftDir, consumerPath);
|
|
2116
|
+
write(
|
|
2117
|
+
consumerFile,
|
|
2118
|
+
fs.readFileSync(consumerFile, 'utf8').replace(
|
|
2119
|
+
`- ${ownerPath}\n\n## Verification Plan`,
|
|
2120
|
+
'- none\n\n## Verification Plan',
|
|
2121
|
+
),
|
|
2122
|
+
);
|
|
2123
|
+
const drifted = run(VALIDATOR, [driftDir], ROOT);
|
|
2124
|
+
assert.notEqual(drifted.status, 0);
|
|
2125
|
+
assert.match(output(drifted), /dependency drift between spec\.json task_registry/);
|
|
2126
|
+
|
|
2127
|
+
const duplicateHeader = createSpec(root, {
|
|
2128
|
+
name: 'duplicate-dependency-section',
|
|
2129
|
+
task: `${task({ related: '`src/duplicate.js` | Read' })}\n## Dependencies\n\n- none\n`,
|
|
2130
|
+
});
|
|
2131
|
+
const duplicateHeaderResult = run(VALIDATOR, [duplicateHeader], ROOT);
|
|
2132
|
+
assert.notEqual(duplicateHeaderResult.status, 0);
|
|
2133
|
+
assert.match(output(duplicateHeaderResult), /machine-read section Dependencies.*must appear at most once/);
|
|
2134
|
+
|
|
2135
|
+
const duplicateSections = createSpec(root, {
|
|
2136
|
+
name: 'duplicate-machine-sections',
|
|
2137
|
+
task: `${task({ related: '`src/duplicate-sections.js` | Read' })}\n` +
|
|
2138
|
+
'## Verification Plan\n- **Command:** `npm test`\n\n' +
|
|
2139
|
+
'## Acceptance\n- **R1.1:** conflicting duplicate mapping\n',
|
|
2140
|
+
});
|
|
2141
|
+
const duplicateSectionsResult = run(VALIDATOR, [duplicateSections], ROOT);
|
|
2142
|
+
assert.notEqual(duplicateSectionsResult.status, 0);
|
|
2143
|
+
assert.match(output(duplicateSectionsResult), /machine-read section Verification Plan.*must appear at most once/);
|
|
2144
|
+
assert.match(output(duplicateSectionsResult), /machine-read section Acceptance.*must appear at most once/);
|
|
2145
|
+
|
|
2146
|
+
const ordinary = createSpec(root, {
|
|
2147
|
+
name: 'ordinary-in-progress',
|
|
2148
|
+
task: task({ related: '`src/ordinary.js` | Read', status: 'in_progress' }),
|
|
2149
|
+
spec: {
|
|
2150
|
+
task_registry: {
|
|
2151
|
+
[ownerPath]: {
|
|
2152
|
+
id: 'R1-01', title: 'One', status: 'in_progress', dependencies: [], blocker: null,
|
|
2153
|
+
started_at: '2026-08-12T00:00:00+07:00', completed_at: null,
|
|
2154
|
+
last_updated_at: '2026-08-12T00:01:00+07:00',
|
|
2155
|
+
artifacts: ['test/one.test.js'],
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2158
|
+
},
|
|
2159
|
+
});
|
|
2160
|
+
const ordinaryResult = run(VALIDATOR, [ordinary], ROOT);
|
|
2161
|
+
assert.equal(ordinaryResult.status, 0, output(ordinaryResult));
|
|
2162
|
+
|
|
2163
|
+
const ordinaryStatePath = path.join(ordinary, 'spec.json');
|
|
2164
|
+
const ordinaryState = JSON.parse(fs.readFileSync(ordinaryStatePath, 'utf8'));
|
|
2165
|
+
const ordinaryTaskPath = path.join(ordinary, ownerPath);
|
|
2166
|
+
write(
|
|
2167
|
+
ordinaryTaskPath,
|
|
2168
|
+
fs.readFileSync(ordinaryTaskPath, 'utf8')
|
|
2169
|
+
.replace(/\n## Dependencies\n[\s\S]*?(?=\n## Verification Plan)/, ''),
|
|
2170
|
+
);
|
|
2171
|
+
ordinaryState.ready_for_implementation = true;
|
|
2172
|
+
ordinaryState.validation = { status: 'completed' };
|
|
2173
|
+
ordinaryState.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
|
|
2174
|
+
write(ordinaryStatePath, JSON.stringify(ordinaryState, null, 2));
|
|
2175
|
+
const readyWithoutHeader = run(VALIDATOR, [ordinary], ROOT);
|
|
2176
|
+
assert.notEqual(readyWithoutHeader.status, 0);
|
|
2177
|
+
assert.match(output(readyWithoutHeader), /missing Dependencies/);
|
|
2178
|
+
|
|
2179
|
+
const invalidCases = [
|
|
2180
|
+
['bad-ready-type', (state) => { state.ready_for_implementation = 'true'; }, /ready_for_implementation: must be a boolean/],
|
|
2181
|
+
['bad-spec-status', (state) => { state.status = 'ready'; }, /spec\.json\.status: must be one of/],
|
|
2182
|
+
['bad-task-status', (state) => { state.task_registry[ownerPath].status = 'ready'; }, /task_registry.*status: must be one of/],
|
|
2183
|
+
['bad-validation-shape', (state) => { state.validation = 'not-run'; }, /validation: must be an object/],
|
|
2184
|
+
[
|
|
2185
|
+
'registry-id-path-drift',
|
|
2186
|
+
(state) => { state.task_registry[ownerPath].id = 'R9-99'; },
|
|
2187
|
+
/task_registry id must match path-derived id R1-01/,
|
|
2188
|
+
],
|
|
2189
|
+
[
|
|
2190
|
+
'markdown-heading-id-drift',
|
|
2191
|
+
(_state, specDir) => {
|
|
2192
|
+
const taskFile = path.join(specDir, ownerPath);
|
|
2193
|
+
write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('# Task R1-01:', '# Task R9-99:'));
|
|
2194
|
+
},
|
|
2195
|
+
/Markdown heading id R9-99 must match path-derived id R1-01/,
|
|
2196
|
+
],
|
|
2197
|
+
[
|
|
2198
|
+
'registry-markdown-status-drift',
|
|
2199
|
+
(state) => {
|
|
2200
|
+
state.task_registry[ownerPath].status = 'done';
|
|
2201
|
+
state.task_registry[ownerPath].started_at = '2026-08-12T00:00:00+07:00';
|
|
2202
|
+
state.task_registry[ownerPath].completed_at = '2026-08-12T00:01:00+07:00';
|
|
2203
|
+
state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
|
|
2204
|
+
},
|
|
2205
|
+
/status drift between spec\.json task_registry done and Markdown Status header pending/,
|
|
2206
|
+
],
|
|
2207
|
+
[
|
|
2208
|
+
'done-missing-lifecycle-timestamps',
|
|
2209
|
+
(state, specDir) => {
|
|
2210
|
+
state.task_registry[ownerPath].status = 'done';
|
|
2211
|
+
const taskFile = path.join(specDir, ownerPath);
|
|
2212
|
+
write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** done'));
|
|
2213
|
+
},
|
|
2214
|
+
/done status requires started_at, completed_at, and last_updated_at timestamps/,
|
|
2215
|
+
],
|
|
2216
|
+
[
|
|
2217
|
+
'blocked-missing-blocker',
|
|
2218
|
+
(state, specDir) => {
|
|
2219
|
+
state.task_registry[ownerPath].status = 'blocked';
|
|
2220
|
+
state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
|
|
2221
|
+
const taskFile = path.join(specDir, ownerPath);
|
|
2222
|
+
write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
|
|
2223
|
+
},
|
|
2224
|
+
/blocked status requires a non-empty blocker/,
|
|
2225
|
+
],
|
|
2226
|
+
[
|
|
2227
|
+
'in-progress-with-blocker',
|
|
2228
|
+
(state, specDir) => {
|
|
2229
|
+
state.task_registry[ownerPath].status = 'in_progress';
|
|
2230
|
+
state.task_registry[ownerPath].blocker = 'waiting on owner';
|
|
2231
|
+
state.task_registry[ownerPath].started_at = '2026-08-12T00:00:00+07:00';
|
|
2232
|
+
state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
|
|
2233
|
+
const taskFile = path.join(specDir, ownerPath);
|
|
2234
|
+
write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** in_progress'));
|
|
2235
|
+
},
|
|
2236
|
+
/in_progress status requires blocker null/,
|
|
2237
|
+
],
|
|
2238
|
+
[
|
|
2239
|
+
'in-progress-reversed-timestamps',
|
|
2240
|
+
(state, specDir) => {
|
|
2241
|
+
state.task_registry[ownerPath].status = 'in_progress';
|
|
2242
|
+
state.task_registry[ownerPath].started_at = '2026-08-12T00:02:00+07:00';
|
|
2243
|
+
state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
|
|
2244
|
+
const taskFile = path.join(specDir, ownerPath);
|
|
2245
|
+
write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** in_progress'));
|
|
2246
|
+
},
|
|
2247
|
+
/in_progress timestamps must satisfy started_at <= last_updated_at/,
|
|
2248
|
+
],
|
|
2249
|
+
[
|
|
2250
|
+
'started-blocked-reversed-timestamps',
|
|
2251
|
+
(state, specDir) => {
|
|
2252
|
+
state.task_registry[ownerPath].status = 'blocked';
|
|
2253
|
+
state.task_registry[ownerPath].blocker = 'waiting for the declared external authority';
|
|
2254
|
+
state.task_registry[ownerPath].started_at = '2026-08-12T00:02:00+07:00';
|
|
2255
|
+
state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
|
|
2256
|
+
const taskFile = path.join(specDir, ownerPath);
|
|
2257
|
+
write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
|
|
2258
|
+
},
|
|
2259
|
+
/started blocked timestamps must satisfy started_at <= last_updated_at/,
|
|
2260
|
+
],
|
|
2261
|
+
[
|
|
2262
|
+
'bad-validation-status',
|
|
2263
|
+
(state) => {
|
|
2264
|
+
state.ready_for_implementation = true;
|
|
2265
|
+
state.validation = { status: 'ready' };
|
|
2266
|
+
state.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
|
|
2267
|
+
},
|
|
2268
|
+
/validation\.status: must be one of|requires validation\.status completed/,
|
|
2269
|
+
],
|
|
2270
|
+
[
|
|
2271
|
+
'blocked-ready',
|
|
2272
|
+
(state, specDir) => {
|
|
2273
|
+
state.ready_for_implementation = true;
|
|
2274
|
+
state.status = 'blocked';
|
|
2275
|
+
state.blocker = 'owner unavailable';
|
|
2276
|
+
state.validation = { status: 'completed' };
|
|
2277
|
+
state.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
|
|
2278
|
+
state.task_registry[ownerPath].status = 'blocked';
|
|
2279
|
+
const taskFile = path.join(specDir, ownerPath);
|
|
2280
|
+
write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
|
|
2281
|
+
},
|
|
2282
|
+
/cannot be blocked while ready_for_implementation is true/,
|
|
2283
|
+
],
|
|
2284
|
+
];
|
|
2285
|
+
for (const [name, mutate, expected] of invalidCases) {
|
|
2286
|
+
const specDir = createSpec(root, {
|
|
2287
|
+
name,
|
|
2288
|
+
task: task({ related: '`src/status.js` | Read' }),
|
|
2289
|
+
});
|
|
2290
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
2291
|
+
const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
2292
|
+
mutate(state, specDir);
|
|
2293
|
+
write(specPath, JSON.stringify(state, null, 2));
|
|
2294
|
+
const result = run(VALIDATOR, [specDir], ROOT);
|
|
2295
|
+
assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
|
|
2296
|
+
assert.match(output(result), expected);
|
|
2297
|
+
}
|
|
2298
|
+
} finally {
|
|
2299
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
test('v1 Critical implementation obligations validate ownership, references, closure, and proof', () => {
|
|
2304
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-obligations-'));
|
|
2305
|
+
const ownerPath = 'tasks/task-R1-01-one.md';
|
|
2306
|
+
const consumerPath = 'tasks/task-R1-02-two.md';
|
|
2307
|
+
const criticalPolicy = legacyWorkflowPolicy('Critical');
|
|
2308
|
+
|
|
2309
|
+
function makeFixture(name, mutate = () => {}, policy = criticalPolicy) {
|
|
2310
|
+
const obligation = {
|
|
2311
|
+
id: 'canonical-transition',
|
|
2312
|
+
requirements: ['R1.1'],
|
|
2313
|
+
canonical_state: 'Persisted transition state and allowed values',
|
|
2314
|
+
authority: 'Owner module is the sole transition authority',
|
|
2315
|
+
recovery: 'Retry reconciles the persisted state before continuing',
|
|
2316
|
+
owner_task: ownerPath,
|
|
2317
|
+
consumer_tasks: [consumerPath],
|
|
2318
|
+
verification: 'node --test focused.test.js',
|
|
2319
|
+
evidence: 'Exit 0 and the declared state transition is observed',
|
|
2320
|
+
};
|
|
2321
|
+
const specDir = createSpec(root, {
|
|
2322
|
+
name,
|
|
2323
|
+
task: task({ related: '`src/owner.js` | Read' }),
|
|
2324
|
+
spec: { workflow_policy: policy },
|
|
2325
|
+
});
|
|
2326
|
+
const specPath = path.join(specDir, 'spec.json');
|
|
2327
|
+
write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n\n- The auth-risk fixture requires independent grounding before implementation.\n');
|
|
2328
|
+
const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
2329
|
+
state.task_files.push(consumerPath);
|
|
2330
|
+
state.task_registry[consumerPath] = {
|
|
2331
|
+
id: 'R1-02', title: 'Two', status: 'pending', dependencies: [ownerPath], blocker: null,
|
|
2332
|
+
started_at: null, completed_at: null, last_updated_at: null,
|
|
2333
|
+
};
|
|
2334
|
+
write(
|
|
2335
|
+
path.join(specDir, consumerPath),
|
|
2336
|
+
task({
|
|
2337
|
+
id: 'R1-02',
|
|
2338
|
+
related: '`src/consumer.js` | Read',
|
|
2339
|
+
dependencies: ownerPath,
|
|
2340
|
+
extra: executionClosure(obligation),
|
|
2341
|
+
})
|
|
2342
|
+
.replace('# Task R1-01: One', '# Task R1-02: Two'),
|
|
2343
|
+
);
|
|
2344
|
+
const ownerFile = path.join(specDir, ownerPath);
|
|
2345
|
+
write(
|
|
2346
|
+
ownerFile,
|
|
2347
|
+
`${fs.readFileSync(ownerFile, 'utf8')}${executionClosure(obligation)}`,
|
|
2348
|
+
);
|
|
2349
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2350
|
+
write(
|
|
2351
|
+
designFile,
|
|
2352
|
+
`${fs.readFileSync(designFile, 'utf8')}${executionClosure(obligation)}`,
|
|
2353
|
+
);
|
|
2354
|
+
state.implementation_obligations = [obligation];
|
|
2355
|
+
mutate(state, specDir);
|
|
2356
|
+
write(specPath, JSON.stringify(state, null, 2));
|
|
2357
|
+
return specDir;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
try {
|
|
2361
|
+
const omitted = createSpec(root, {
|
|
2362
|
+
name: 'critical-omitted-obligation-decision',
|
|
2363
|
+
spec: { workflow_policy: criticalPolicy },
|
|
2364
|
+
});
|
|
2365
|
+
write(path.join(omitted, 'research.md'), '# Research\n\n## Evidence Summary\n\n- The auth-risk fixture requires independent grounding before implementation.\n');
|
|
2366
|
+
const omittedResult = run(VALIDATOR, [omitted], ROOT);
|
|
2367
|
+
assert.notEqual(omittedResult.status, 0);
|
|
2368
|
+
assert.match(output(omittedResult), /Critical scope must declare non-empty.*or one justified N\/A/);
|
|
2369
|
+
|
|
2370
|
+
const valid = makeFixture('valid-obligation');
|
|
2371
|
+
const validResult = run(VALIDATOR, [valid], ROOT);
|
|
2372
|
+
assert.equal(validResult.status, 0, output(validResult));
|
|
2373
|
+
|
|
2374
|
+
const tableClosure = makeFixture('valid-table-closure', (state, specDir) => {
|
|
2375
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2376
|
+
const designPrefix = fs.readFileSync(designFile, 'utf8').split('\n## Execution Closure\n')[0];
|
|
2377
|
+
write(designFile, `${designPrefix}${executionClosureTable(state.implementation_obligations[0])}`);
|
|
2378
|
+
});
|
|
2379
|
+
const tableClosureResult = run(VALIDATOR, [tableClosure], ROOT);
|
|
2380
|
+
assert.equal(tableClosureResult.status, 0, output(tableClosureResult));
|
|
2381
|
+
|
|
2382
|
+
const swapped = makeFixture('marker-local-swapped-obligations', (state, specDir) => {
|
|
2383
|
+
const first = state.implementation_obligations[0];
|
|
2384
|
+
const second = {
|
|
2385
|
+
...first,
|
|
2386
|
+
id: 'secondary-transition',
|
|
2387
|
+
canonical_state: 'Secondary persisted state and terminal values',
|
|
2388
|
+
authority: 'Secondary module is the sole transition authority',
|
|
2389
|
+
recovery: 'Restart rolls the secondary state back before retry',
|
|
2390
|
+
verification: 'node --test secondary-focused.test.js',
|
|
2391
|
+
evidence: 'Exit 0 and the secondary persisted transition is observed',
|
|
2392
|
+
};
|
|
2393
|
+
state.implementation_obligations.push(second);
|
|
2394
|
+
for (const taskFile of [ownerPath, consumerPath]) {
|
|
2395
|
+
const filePath = path.join(specDir, taskFile);
|
|
2396
|
+
write(filePath, `${fs.readFileSync(filePath, 'utf8')}\n${obligationClosureBlock(second)}`);
|
|
2397
|
+
}
|
|
2398
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2399
|
+
const designPrefix = fs.readFileSync(designFile, 'utf8').split('\n## Execution Closure\n')[0];
|
|
2400
|
+
const firstWithSecondValues = { ...second, id: first.id };
|
|
2401
|
+
const secondWithFirstValues = { ...first, id: second.id };
|
|
2402
|
+
write(
|
|
2403
|
+
designFile,
|
|
2404
|
+
`${designPrefix}\n## Execution Closure\n\n` +
|
|
2405
|
+
`${obligationClosureBlock(firstWithSecondValues)}\n${obligationClosureBlock(secondWithFirstValues)}`,
|
|
2406
|
+
);
|
|
2407
|
+
});
|
|
2408
|
+
const swappedResult = run(VALIDATOR, [swapped], ROOT);
|
|
2409
|
+
assert.notEqual(swappedResult.status, 0);
|
|
2410
|
+
assert.match(output(swappedResult), /marker-local field/);
|
|
2411
|
+
|
|
2412
|
+
const cases = [
|
|
2413
|
+
[
|
|
2414
|
+
'standard-only-rejected',
|
|
2415
|
+
() => {},
|
|
2416
|
+
legacyWorkflowPolicy(),
|
|
2417
|
+
/allowed only for Critical workflow policy/,
|
|
2418
|
+
],
|
|
2419
|
+
[
|
|
2420
|
+
'owner-must-be-one-task',
|
|
2421
|
+
(state) => { state.implementation_obligations[0].owner_task = [ownerPath]; },
|
|
2422
|
+
criticalPolicy,
|
|
2423
|
+
/owner_task: must name exactly one registered task/,
|
|
2424
|
+
],
|
|
2425
|
+
[
|
|
2426
|
+
'unknown-requirement',
|
|
2427
|
+
(state) => { state.implementation_obligations[0].requirements = ['R9.1']; },
|
|
2428
|
+
criticalPolicy,
|
|
2429
|
+
/unknown requirement reference R9\.1/,
|
|
2430
|
+
],
|
|
2431
|
+
[
|
|
2432
|
+
'unknown-consumer',
|
|
2433
|
+
(state) => { state.implementation_obligations[0].consumer_tasks = ['tasks/task-R1-99-missing.md']; },
|
|
2434
|
+
criticalPolicy,
|
|
2435
|
+
/unknown task reference tasks\/task-R1-99-missing\.md/,
|
|
2436
|
+
],
|
|
2437
|
+
[
|
|
2438
|
+
'missing-dependency-closure',
|
|
2439
|
+
(state, specDir) => {
|
|
2440
|
+
state.task_registry[consumerPath].dependencies = [];
|
|
2441
|
+
const taskFile = path.join(specDir, consumerPath);
|
|
2442
|
+
write(
|
|
2443
|
+
taskFile,
|
|
2444
|
+
fs.readFileSync(taskFile, 'utf8').replace(
|
|
2445
|
+
`- ${ownerPath}\n\n## Verification Plan`,
|
|
2446
|
+
'- none\n\n## Verification Plan',
|
|
2447
|
+
),
|
|
2448
|
+
);
|
|
2449
|
+
},
|
|
2450
|
+
criticalPolicy,
|
|
2451
|
+
/must depend on owner tasks\/task-R1-01-one\.md/,
|
|
2452
|
+
],
|
|
2453
|
+
[
|
|
2454
|
+
'missing-proof',
|
|
2455
|
+
(state) => {
|
|
2456
|
+
state.implementation_obligations[0].verification = 'TBD';
|
|
2457
|
+
state.implementation_obligations[0].evidence = 'PENDING';
|
|
2458
|
+
},
|
|
2459
|
+
criticalPolicy,
|
|
2460
|
+
/verification: must be an executable command|evidence: must name an observable result/,
|
|
2461
|
+
],
|
|
2462
|
+
[
|
|
2463
|
+
'missing-state-authority-recovery',
|
|
2464
|
+
(state) => {
|
|
2465
|
+
delete state.implementation_obligations[0].canonical_state;
|
|
2466
|
+
state.implementation_obligations[0].authority = 'authority';
|
|
2467
|
+
state.implementation_obligations[0].recovery = 'recovery';
|
|
2468
|
+
},
|
|
2469
|
+
criticalPolicy,
|
|
2470
|
+
/canonical_state: must be concrete|authority: must be concrete|recovery: must be concrete/,
|
|
2471
|
+
],
|
|
2472
|
+
[
|
|
2473
|
+
'missing-design-correspondence',
|
|
2474
|
+
(_state, specDir) => {
|
|
2475
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2476
|
+
write(
|
|
2477
|
+
designFile,
|
|
2478
|
+
fs.readFileSync(designFile, 'utf8').replace(
|
|
2479
|
+
'\n<!-- implementation-obligation:canonical-transition -->\n',
|
|
2480
|
+
'\n',
|
|
2481
|
+
),
|
|
2482
|
+
);
|
|
2483
|
+
},
|
|
2484
|
+
criticalPolicy,
|
|
2485
|
+
/design\.md: missing implementation obligation marker canonical-transition/,
|
|
2486
|
+
],
|
|
2487
|
+
[
|
|
2488
|
+
'missing-owner-correspondence',
|
|
2489
|
+
(_state, specDir) => {
|
|
2490
|
+
const ownerFile = path.join(specDir, ownerPath);
|
|
2491
|
+
write(
|
|
2492
|
+
ownerFile,
|
|
2493
|
+
fs.readFileSync(ownerFile, 'utf8').replace(
|
|
2494
|
+
'\n<!-- implementation-obligation:canonical-transition -->\n',
|
|
2495
|
+
'\n',
|
|
2496
|
+
),
|
|
2497
|
+
);
|
|
2498
|
+
},
|
|
2499
|
+
criticalPolicy,
|
|
2500
|
+
/task-R1-01-one\.md: missing implementation obligation marker canonical-transition/,
|
|
2501
|
+
],
|
|
2502
|
+
[
|
|
2503
|
+
'marker-outside-execution-closure',
|
|
2504
|
+
(_state, specDir) => {
|
|
2505
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2506
|
+
const body = fs.readFileSync(designFile, 'utf8')
|
|
2507
|
+
.replace('\n<!-- implementation-obligation:canonical-transition -->\n', '\n');
|
|
2508
|
+
write(designFile, `${body}\n## Outside Closure\n\n<!-- implementation-obligation:canonical-transition -->\n`);
|
|
2509
|
+
},
|
|
2510
|
+
criticalPolicy,
|
|
2511
|
+
/implementation obligation marker canonical-transition must be inside Execution Closure/,
|
|
2512
|
+
],
|
|
2513
|
+
[
|
|
2514
|
+
'duplicate-closure-marker',
|
|
2515
|
+
(_state, specDir) => {
|
|
2516
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2517
|
+
write(
|
|
2518
|
+
designFile,
|
|
2519
|
+
fs.readFileSync(designFile, 'utf8').replace(
|
|
2520
|
+
'<!-- implementation-obligation:canonical-transition -->',
|
|
2521
|
+
'<!-- implementation-obligation:canonical-transition -->\n<!-- implementation-obligation:canonical-transition -->',
|
|
2522
|
+
),
|
|
2523
|
+
);
|
|
2524
|
+
},
|
|
2525
|
+
criticalPolicy,
|
|
2526
|
+
/duplicate implementation obligation marker canonical-transition/,
|
|
2527
|
+
],
|
|
2528
|
+
[
|
|
2529
|
+
'duplicate-execution-closure',
|
|
2530
|
+
(_state, specDir) => {
|
|
2531
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2532
|
+
write(designFile, `${fs.readFileSync(designFile, 'utf8')}\n## Execution Closure\n\nNo duplicate allowed.\n`);
|
|
2533
|
+
},
|
|
2534
|
+
criticalPolicy,
|
|
2535
|
+
/implementation obligation markers require exactly one Execution Closure section/,
|
|
2536
|
+
],
|
|
2537
|
+
[
|
|
2538
|
+
'closure-json-drift',
|
|
2539
|
+
(_state, specDir) => {
|
|
2540
|
+
const ownerFile = path.join(specDir, ownerPath);
|
|
2541
|
+
write(
|
|
2542
|
+
ownerFile,
|
|
2543
|
+
fs.readFileSync(ownerFile, 'utf8').replace(
|
|
2544
|
+
'Persisted transition state and allowed values',
|
|
2545
|
+
'A divergent local transition description',
|
|
2546
|
+
),
|
|
2547
|
+
);
|
|
2548
|
+
},
|
|
2549
|
+
criticalPolicy,
|
|
2550
|
+
/task-R1-01-one\.md: Execution Closure for canonical-transition must mirror spec\.json canonical_state/,
|
|
2551
|
+
],
|
|
2552
|
+
[
|
|
2553
|
+
'closure-requirements-drift',
|
|
2554
|
+
(_state, specDir) => {
|
|
2555
|
+
const designFile = path.join(specDir, 'design.md');
|
|
2556
|
+
write(
|
|
2557
|
+
designFile,
|
|
2558
|
+
fs.readFileSync(designFile, 'utf8').replace('- Requirements: R1.1', '- Requirements: R9.1'),
|
|
2559
|
+
);
|
|
2560
|
+
},
|
|
2561
|
+
criticalPolicy,
|
|
2562
|
+
/design\.md: Execution Closure for canonical-transition must mirror spec\.json requirements/,
|
|
2563
|
+
],
|
|
2564
|
+
[
|
|
2565
|
+
'duplicate-obligation-id',
|
|
2566
|
+
(state) => {
|
|
2567
|
+
state.implementation_obligations.push({ ...state.implementation_obligations[0] });
|
|
2568
|
+
},
|
|
2569
|
+
criticalPolicy,
|
|
2570
|
+
/duplicate obligation id canonical-transition/,
|
|
2571
|
+
],
|
|
2572
|
+
[
|
|
2573
|
+
'whitespace-obligation-id',
|
|
2574
|
+
(state) => { state.implementation_obligations[0].id = ' canonical-transition '; },
|
|
2575
|
+
criticalPolicy,
|
|
2576
|
+
/must be lowercase without surrounding whitespace/,
|
|
2577
|
+
],
|
|
2578
|
+
];
|
|
2579
|
+
for (const [name, mutate, policy, expected] of cases) {
|
|
2580
|
+
const specDir = makeFixture(name, mutate, policy);
|
|
2581
|
+
const result = run(VALIDATOR, [specDir], ROOT);
|
|
2582
|
+
assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
|
|
2583
|
+
assert.match(output(result), expected);
|
|
2584
|
+
}
|
|
2585
|
+
} finally {
|
|
2586
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
|
|
926
2590
|
test('grounding rejects empty sections, unsafe paths, unsupported actions, and zero-match globs', () => {
|
|
927
2591
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-grounding-'));
|
|
928
2592
|
try {
|
|
@@ -958,7 +2622,10 @@ test('validator enforces Read before Modify for existing implementation', () =>
|
|
|
958
2622
|
// Existing file Modify without preceding Read must fail
|
|
959
2623
|
const missingRead = createSpec(root, {
|
|
960
2624
|
name: 'missing-read',
|
|
961
|
-
task: task({
|
|
2625
|
+
task: task({
|
|
2626
|
+
related: '`src/existing.js` | Modify',
|
|
2627
|
+
relatedRows: '| `src/existing.js` | Modify | Update existing implementation |',
|
|
2628
|
+
}),
|
|
962
2629
|
});
|
|
963
2630
|
const missingResult = run(VALIDATOR, [missingRead], ROOT);
|
|
964
2631
|
assert.notEqual(missingResult.status, 0);
|
|
@@ -967,7 +2634,7 @@ test('validator enforces Read before Modify for existing implementation', () =>
|
|
|
967
2634
|
// Same path with Read in same task earlier row should pass
|
|
968
2635
|
const withReadSameTask = createSpec(root, {
|
|
969
2636
|
name: 'with-read-same',
|
|
970
|
-
task:
|
|
2637
|
+
task: task({ related: '`src/existing.js` | Modify' }),
|
|
971
2638
|
});
|
|
972
2639
|
const sameResult = run(VALIDATOR, [withReadSameTask], ROOT);
|
|
973
2640
|
assert.equal(sameResult.status, 0, output(sameResult));
|
|
@@ -979,8 +2646,13 @@ test('validator enforces Read before Modify for existing implementation', () =>
|
|
|
979
2646
|
depState.task_files.push('tasks/task-R1-02-two.md');
|
|
980
2647
|
depState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
|
|
981
2648
|
// Task 1 has Read, Task 2 modifies without depending
|
|
982
|
-
write(path.join(depRead, 'tasks/task-R1-01-one.md'),
|
|
983
|
-
write(path.join(depRead, 'tasks/task-R1-02-two.md'), task({
|
|
2649
|
+
write(path.join(depRead, 'tasks/task-R1-01-one.md'), task({ related: '`src/existing.js` | Read' }));
|
|
2650
|
+
write(path.join(depRead, 'tasks/task-R1-02-two.md'), task({
|
|
2651
|
+
id: 'R1-02',
|
|
2652
|
+
related: '`src/existing.js` | Modify',
|
|
2653
|
+
relatedRows: '| `src/existing.js` | Modify | Update existing implementation |',
|
|
2654
|
+
dependencies: 'tasks/task-R1-01-one.md',
|
|
2655
|
+
}));
|
|
984
2656
|
write(depStatePath, JSON.stringify(depState, null, 2));
|
|
985
2657
|
const depMissingResult = run(VALIDATOR, [depRead], ROOT);
|
|
986
2658
|
assert.notEqual(depMissingResult.status, 0);
|
|
@@ -988,6 +2660,7 @@ test('validator enforces Read before Modify for existing implementation', () =>
|
|
|
988
2660
|
|
|
989
2661
|
// If Modify task depends on Read task, should pass
|
|
990
2662
|
depState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
|
|
2663
|
+
depState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
|
|
991
2664
|
write(depStatePath, JSON.stringify(depState, null, 2));
|
|
992
2665
|
const depPass = run(VALIDATOR, [depRead], ROOT);
|
|
993
2666
|
assert.equal(depPass.status, 0, output(depPass));
|
|
@@ -1018,17 +2691,22 @@ test('validator rejects reachability without concrete anchor and accepts with pa
|
|
|
1018
2691
|
}
|
|
1019
2692
|
});
|
|
1020
2693
|
|
|
1021
|
-
test('validator
|
|
2694
|
+
test('validator keeps contract blocks opt-in regardless of task count', () => {
|
|
1022
2695
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-contract-closed-'));
|
|
1023
2696
|
try {
|
|
1024
2697
|
const specDir = createSpec(root, { name: 'five-no-contract' });
|
|
1025
2698
|
const statePath = path.join(specDir, 'spec.json');
|
|
1026
2699
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
2700
|
+
state.design_context = { validation_recommended: true };
|
|
1027
2701
|
for (let i = 2; i <= 5; i += 1) {
|
|
1028
2702
|
const taskFile = `tasks/task-R1-0${i}-extra.md`;
|
|
1029
2703
|
state.task_files.push(taskFile);
|
|
1030
2704
|
state.task_registry[taskFile] = { id: `R1-0${i}`, title: `Extra ${i}`, status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
|
|
1031
|
-
write(path.join(specDir, taskFile), task({
|
|
2705
|
+
write(path.join(specDir, taskFile), task({
|
|
2706
|
+
id: `R1-0${i}`,
|
|
2707
|
+
related: '`src/extra.js` | Modify',
|
|
2708
|
+
mapping: '1.1',
|
|
2709
|
+
}));
|
|
1032
2710
|
}
|
|
1033
2711
|
// Need to ensure each extra task has Read before Modify to avoid lifecycle error
|
|
1034
2712
|
for (let i = 2; i <= 5; i += 1) {
|
|
@@ -1042,8 +2720,7 @@ test('validator fails closed when 5+ tasks have no contract blocks', () => {
|
|
|
1042
2720
|
write(path.join(specDir, 'tasks/task-R1-01-one.md'), first);
|
|
1043
2721
|
write(statePath, JSON.stringify(state, null, 2));
|
|
1044
2722
|
const result = run(VALIDATOR, [specDir], ROOT);
|
|
1045
|
-
assert.
|
|
1046
|
-
assert.match(output(result), /5\+ task spec requires contract blocks/);
|
|
2723
|
+
assert.equal(result.status, 0, output(result));
|
|
1047
2724
|
} finally {
|
|
1048
2725
|
fs.rmSync(root, { recursive: true, force: true });
|
|
1049
2726
|
}
|
|
@@ -1085,7 +2762,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
|
|
|
1085
2762
|
// Same-task: Create after Read must fail (dead-code regression)
|
|
1086
2763
|
const createAfterRead = createSpec(root, {
|
|
1087
2764
|
name: 'create-after-read',
|
|
1088
|
-
task:
|
|
2765
|
+
task: task({
|
|
2766
|
+
related: '`src/new.js` | Read',
|
|
2767
|
+
relatedRows: '| `src/new.js` | Read | Read before create |\n| `src/new.js` | Create | Create after read |',
|
|
2768
|
+
}),
|
|
1089
2769
|
});
|
|
1090
2770
|
const carResult = run(VALIDATOR, [createAfterRead], ROOT);
|
|
1091
2771
|
assert.notEqual(carResult.status, 0);
|
|
@@ -1094,7 +2774,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
|
|
|
1094
2774
|
// Same-task: Create after Modify must fail
|
|
1095
2775
|
const createAfterModify = createSpec(root, {
|
|
1096
2776
|
name: 'create-after-modify',
|
|
1097
|
-
task:
|
|
2777
|
+
task: task({
|
|
2778
|
+
related: '`src/new.js` | Modify',
|
|
2779
|
+
relatedRows: '| `src/new.js` | Modify | Modify before create |\n| `src/new.js` | Create | Create after modify |',
|
|
2780
|
+
}),
|
|
1098
2781
|
});
|
|
1099
2782
|
const camResult = run(VALIDATOR, [createAfterModify], ROOT);
|
|
1100
2783
|
assert.notEqual(camResult.status, 0);
|
|
@@ -1103,7 +2786,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
|
|
|
1103
2786
|
// Same-task: Create before Read/Modify must pass; repeated Reads must not be rejected
|
|
1104
2787
|
const createBefore = createSpec(root, {
|
|
1105
2788
|
name: 'create-before',
|
|
1106
|
-
task:
|
|
2789
|
+
task: task({
|
|
2790
|
+
related: '`src/new.js` | Create',
|
|
2791
|
+
relatedRows: '| `src/new.js` | Create | Create first |\n| `src/new.js` | Read | Read after create |\n| `src/new.js` | Modify | Modify after create |\n| `src/new.js` | Read | Second read |',
|
|
2792
|
+
}),
|
|
1107
2793
|
});
|
|
1108
2794
|
const cbResult = run(VALIDATOR, [createBefore], ROOT);
|
|
1109
2795
|
assert.equal(cbResult.status, 0, output(cbResult));
|
|
@@ -1111,7 +2797,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
|
|
|
1111
2797
|
// Valid same-task repeated reads without create must pass (not rejected unnecessarily)
|
|
1112
2798
|
const repeatedReads = createSpec(root, {
|
|
1113
2799
|
name: 'repeated-reads',
|
|
1114
|
-
task:
|
|
2800
|
+
task: task({
|
|
2801
|
+
related: '`src/existing.js` | Read',
|
|
2802
|
+
relatedRows: '| `src/existing.js` | Read | First read |\n| `src/existing.js` | Read | Second read |\n| `src/existing.js` | Modify | Modify after reads |',
|
|
2803
|
+
}),
|
|
1115
2804
|
});
|
|
1116
2805
|
const rrResult = run(VALIDATOR, [repeatedReads], ROOT);
|
|
1117
2806
|
assert.equal(rrResult.status, 0, output(rrResult));
|
|
@@ -1121,10 +2810,13 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
|
|
|
1121
2810
|
const crossReadStatePath = path.join(crossRead, 'spec.json');
|
|
1122
2811
|
const crossReadState = JSON.parse(fs.readFileSync(crossReadStatePath, 'utf8'));
|
|
1123
2812
|
// Task1 creates src/gen.js
|
|
1124
|
-
write(path.join(crossRead, 'tasks/task-R1-01-one.md'),
|
|
2813
|
+
write(path.join(crossRead, 'tasks/task-R1-01-one.md'), task({ related: '`src/gen.js` | Create' }));
|
|
1125
2814
|
crossReadState.task_files.push('tasks/task-R1-02-two.md');
|
|
1126
2815
|
crossReadState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
|
|
1127
|
-
write(path.join(crossRead, 'tasks/task-R1-02-two.md'),
|
|
2816
|
+
write(path.join(crossRead, 'tasks/task-R1-02-two.md'), task({
|
|
2817
|
+
id: 'R1-02',
|
|
2818
|
+
related: '`src/gen.js` | Read',
|
|
2819
|
+
}));
|
|
1128
2820
|
write(crossReadStatePath, JSON.stringify(crossReadState, null, 2));
|
|
1129
2821
|
const crossReadFail = run(VALIDATOR, [crossRead], ROOT);
|
|
1130
2822
|
assert.notEqual(crossReadFail.status, 0);
|
|
@@ -1132,6 +2824,13 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
|
|
|
1132
2824
|
|
|
1133
2825
|
// Cross-task Read with dependency must pass
|
|
1134
2826
|
crossReadState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
|
|
2827
|
+
crossReadState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
|
|
2828
|
+
const crossReadTaskPath = path.join(crossRead, 'tasks/task-R1-02-two.md');
|
|
2829
|
+
write(crossReadTaskPath, task({
|
|
2830
|
+
id: 'R1-02',
|
|
2831
|
+
related: '`src/gen.js` | Read',
|
|
2832
|
+
dependencies: 'tasks/task-R1-01-one.md',
|
|
2833
|
+
}));
|
|
1135
2834
|
write(crossReadStatePath, JSON.stringify(crossReadState, null, 2));
|
|
1136
2835
|
const crossReadPass = run(VALIDATOR, [crossRead], ROOT);
|
|
1137
2836
|
assert.equal(crossReadPass.status, 0, output(crossReadPass));
|
|
@@ -1140,15 +2839,27 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
|
|
|
1140
2839
|
const crossMod = createSpec(root, { name: 'cross-mod-base' });
|
|
1141
2840
|
const crossModStatePath = path.join(crossMod, 'spec.json');
|
|
1142
2841
|
const crossModState = JSON.parse(fs.readFileSync(crossModStatePath, 'utf8'));
|
|
1143
|
-
write(path.join(crossMod, 'tasks/task-R1-01-one.md'),
|
|
2842
|
+
write(path.join(crossMod, 'tasks/task-R1-01-one.md'), task({ related: '`src/gen2.js` | Create' }));
|
|
1144
2843
|
crossModState.task_files.push('tasks/task-R1-02-two.md');
|
|
1145
2844
|
crossModState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
|
|
1146
|
-
write(path.join(crossMod, 'tasks/task-R1-02-two.md'),
|
|
2845
|
+
write(path.join(crossMod, 'tasks/task-R1-02-two.md'), task({
|
|
2846
|
+
id: 'R1-02',
|
|
2847
|
+
related: '`src/gen2.js` | Modify',
|
|
2848
|
+
relatedRows: '| `src/gen2.js` | Modify | Modify without dependency |',
|
|
2849
|
+
}));
|
|
1147
2850
|
write(crossModStatePath, JSON.stringify(crossModState, null, 2));
|
|
1148
2851
|
const crossModFail = run(VALIDATOR, [crossMod], ROOT);
|
|
1149
2852
|
assert.notEqual(crossModFail.status, 0);
|
|
1150
2853
|
assert.match(output(crossModFail), /modify of src\/gen2\.js must depend on creator/);
|
|
1151
2854
|
crossModState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
|
|
2855
|
+
crossModState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
|
|
2856
|
+
const crossModTaskPath = path.join(crossMod, 'tasks/task-R1-02-two.md');
|
|
2857
|
+
write(crossModTaskPath, task({
|
|
2858
|
+
id: 'R1-02',
|
|
2859
|
+
related: '`src/gen2.js` | Modify',
|
|
2860
|
+
relatedRows: '| `src/gen2.js` | Modify | Modify generated implementation |',
|
|
2861
|
+
dependencies: 'tasks/task-R1-01-one.md',
|
|
2862
|
+
}));
|
|
1152
2863
|
write(crossModStatePath, JSON.stringify(crossModState, null, 2));
|
|
1153
2864
|
const crossModPass = run(VALIDATOR, [crossMod], ROOT);
|
|
1154
2865
|
assert.equal(crossModPass.status, 0, output(crossModPass));
|
|
@@ -1416,3 +3127,84 @@ test('benchmark validate handles empty and invalid evidence as not-ready/invalid
|
|
|
1416
3127
|
fs.rmSync(root, { recursive: true, force: true });
|
|
1417
3128
|
}
|
|
1418
3129
|
});
|
|
3130
|
+
|
|
3131
|
+
test('D12: semantic-firewall test discovery finds R0-01\'s required basenames in the real checkout', () => {
|
|
3132
|
+
const DISCOVERY = require(path.join(ROOT, 'scripts/semantic-firewall-test-discovery.cjs'));
|
|
3133
|
+
const testsDir = path.join(ROOT, 'bin/__tests__');
|
|
3134
|
+
const discovered = DISCOVERY.discoverSemanticFirewallTests(testsDir);
|
|
3135
|
+
assert.ok(discovered.includes('change-firewall.semantic-firewall.test.js'), 'change-firewall.semantic-firewall.test.js must be discovered');
|
|
3136
|
+
assert.ok(discovered.includes('release-preflight.semantic-firewall.test.js'), 'release-preflight.semantic-firewall.test.js must be discovered');
|
|
3137
|
+
assert.deepEqual(discovered, [...discovered].sort(), 'discovery must return sorted basenames');
|
|
3138
|
+
|
|
3139
|
+
const requiredOk = DISCOVERY.assertRequiredSemanticTests(testsDir, [
|
|
3140
|
+
'change-firewall.semantic-firewall.test.js',
|
|
3141
|
+
'release-preflight.semantic-firewall.test.js',
|
|
3142
|
+
]);
|
|
3143
|
+
assert.equal(requiredOk.ok, true);
|
|
3144
|
+
assert.deepEqual(requiredOk.missing, []);
|
|
3145
|
+
|
|
3146
|
+
const requiredMissing = DISCOVERY.assertRequiredSemanticTests(testsDir, ['does-not-exist.semantic-firewall.test.js']);
|
|
3147
|
+
assert.equal(requiredMissing.ok, false);
|
|
3148
|
+
assert.deepEqual(requiredMissing.missing, ['does-not-exist.semantic-firewall.test.js']);
|
|
3149
|
+
});
|
|
3150
|
+
|
|
3151
|
+
test('D12: run-skill-self-tests.mjs sentinel exits nonzero immediately when a required semantic-firewall basename is missing', () => {
|
|
3152
|
+
const RUNNER = path.join(ROOT, 'scripts/run-skill-self-tests.mjs');
|
|
3153
|
+
const result = spawnSync(process.execPath, [
|
|
3154
|
+
RUNNER, '--require-semantic-test', 'does-not-exist.semantic-firewall.test.js',
|
|
3155
|
+
], { cwd: ROOT, encoding: 'utf8', timeout: 15000 });
|
|
3156
|
+
assert.notEqual(result.status, 0);
|
|
3157
|
+
assert.match(`${result.stdout}${result.stderr}`, /required semantic-firewall test not discovered: does-not-exist\.semantic-firewall\.test\.js/);
|
|
3158
|
+
});
|
|
3159
|
+
|
|
3160
|
+
test('R0-01 owned change-firewall contract surface (D1/C15) is reachable: every named export and owned anchor target exists', () => {
|
|
3161
|
+
const CHANGE_FIREWALL = require(path.join(ROOT, 'src/claude/scripts/change-firewall.cjs'));
|
|
3162
|
+
const requiredExports = [
|
|
3163
|
+
'loadBaselineAuthority', 'assertBaselineUsable', 'bootstrapBaseline', 'advanceBaseline',
|
|
3164
|
+
'deriveProtectedChangedPaths', 'computeProtectedTreeDigest', 'computeProposalDigest',
|
|
3165
|
+
'computeCandidateDigest', 'assertHotspotChangeAllowed', 'createFreezeManifest',
|
|
3166
|
+
'verifyFreezeManifest', 'invalidateFreeze', 'NO_CHANGE_ATTESTATION_DIGEST',
|
|
3167
|
+
'loadFailureLedger', 'assertFailureLedgerUsable', 'openFailure', 'classifyFailure',
|
|
3168
|
+
'reclassifyFailure', 'resolveFailure', 'computeFailureLedgerDigest', 'deriveFailureState',
|
|
3169
|
+
'NO_FAILURES_ATTESTATION_DIGEST', 'createChangeFirewall', 'assertLegacyBridgeArtifact',
|
|
3170
|
+
];
|
|
3171
|
+
for (const name of requiredExports) {
|
|
3172
|
+
assert.ok(Object.prototype.hasOwnProperty.call(CHANGE_FIREWALL, name), `change-firewall.cjs must export ${name}`);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
const ownedAnchorTargets = [
|
|
3176
|
+
'src/claude/scripts/change-firewall.cjs',
|
|
3177
|
+
'src/claude/scripts/spec-authoring-digest.cjs',
|
|
3178
|
+
'benchmarks/change-proposal.schema.json',
|
|
3179
|
+
'benchmarks/change-firewall-freeze.schema.json',
|
|
3180
|
+
'benchmarks/release-baseline.schema.json',
|
|
3181
|
+
'benchmarks/migration-manifest.json',
|
|
3182
|
+
'benchmarks/bootstrap-attestation.schema.json',
|
|
3183
|
+
'benchmarks/benchmark-failure-ledger.schema.json',
|
|
3184
|
+
'scripts/release-preflight.mjs',
|
|
3185
|
+
'scripts/benchmark-workflow.mjs',
|
|
3186
|
+
'scripts/semantic-firewall-test-discovery.cjs',
|
|
3187
|
+
'scripts/run-skill-self-tests.mjs',
|
|
3188
|
+
'bin/__tests__/change-firewall.semantic-firewall.test.js',
|
|
3189
|
+
'bin/__tests__/release-preflight.semantic-firewall.test.js',
|
|
3190
|
+
'package.json',
|
|
3191
|
+
'.gitignore',
|
|
3192
|
+
];
|
|
3193
|
+
for (const relative of ownedAnchorTargets) {
|
|
3194
|
+
assert.ok(fs.existsSync(path.join(ROOT, relative)), `owned anchor target must exist: ${relative}`);
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3197
|
+
|
|
3198
|
+
test('D11/R1.14: the legacy-bridge precondition artifact is reachable and change-firewall.cjs asserts its exact schema', () => {
|
|
3199
|
+
const CHANGE_FIREWALL = require(path.join(ROOT, 'src/claude/scripts/change-firewall.cjs'));
|
|
3200
|
+
// The bridge artifact lives under the monorepo root's specs/ (change-firewall.cjs
|
|
3201
|
+
// resolves its own production root the same way, via git from its own file
|
|
3202
|
+
// location) -- ROOT here is packages/spec, one level below that root.
|
|
3203
|
+
const MONOREPO_ROOT = path.resolve(ROOT, '..', '..');
|
|
3204
|
+
const bridgePath = path.join(MONOREPO_ROOT, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json');
|
|
3205
|
+
assert.ok(fs.existsSync(bridgePath), 'the legacy-bridge artifact must exist before any R0-01 change-firewall assertion runs');
|
|
3206
|
+
const parsed = CHANGE_FIREWALL.assertLegacyBridgeArtifact();
|
|
3207
|
+
assert.equal(parsed.schema_version, '1');
|
|
3208
|
+
assert.equal(parsed.verdict, 'PASS');
|
|
3209
|
+
assert.equal(parsed.blocking_count, 0);
|
|
3210
|
+
});
|