@haposoft/cafekit 0.16.7 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -22
- package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
- package/bin/__tests__/codex-hooks.test.js +169 -272
- package/bin/__tests__/codex-native.test.js +100 -202
- package/bin/__tests__/develop-contract.test.js +122 -1413
- package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
- package/bin/__tests__/grok-envelope.test.js +0 -56
- package/bin/__tests__/hook-payload.test.js +4 -6
- package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
- package/bin/__tests__/omp-bridge.test.js +20 -17
- package/bin/__tests__/omp-hooks.test.js +0 -19
- package/bin/__tests__/optional-skill-inventory.test.js +112 -4
- package/bin/__tests__/orca-session.test.js +0 -2
- package/bin/__tests__/package-inventory.test.js +90 -925
- package/bin/__tests__/runtime-dir.test.js +0 -3
- package/bin/__tests__/runtime-schema.test.js +3 -2
- package/bin/__tests__/skill-routing-source.test.js +27 -15
- package/bin/__tests__/spec-narrowing.test.js +30 -161
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/ui-ux-skill.test.js +101 -0
- package/bin/__tests__/usage-hook-retired.test.js +114 -0
- package/bin/__tests__/validator-grounding.test.js +18 -2949
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/codex-runtime.js +27 -0
- package/bin/phases/copy-payload.js +13 -28
- package/bin/phases/omp-runtime.js +25 -0
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +3 -10
- package/src/claude/agents/brainstormer.md +7 -3
- package/src/claude/agents/code-auditor.md +44 -56
- package/src/claude/agents/debugger.md +8 -88
- package/src/claude/agents/deployer.md +3 -5
- package/src/claude/agents/docs-keeper.md +6 -14
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +12 -16
- package/src/claude/agents/inspector.md +4 -3
- package/src/claude/agents/project-manager.md +9 -17
- package/src/claude/agents/researcher.md +9 -3
- package/src/claude/agents/spec-maker.md +15 -15
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
- package/src/claude/hooks/__tests__/state.test.js +68 -0
- package/src/claude/hooks/__tests__/statusline.test.js +140 -11
- package/src/claude/hooks/agent.cjs +2 -1
- package/src/claude/hooks/lib/hook-payload.cjs +2 -2
- package/src/claude/hooks/rules.cjs +2 -1
- package/src/claude/hooks/session.cjs +0 -1
- package/src/claude/hooks/spec-gate.cjs +17 -83
- package/src/claude/hooks/spec-state.cjs +142 -39
- package/src/claude/hooks/state.cjs +21 -2
- package/src/claude/migration-manifest.json +42 -22
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +2 -3
- package/src/claude/rules/hook-protocols.md +8 -15
- package/src/claude/rules/manage-docs.md +9 -23
- package/src/claude/rules/orchestrator.md +3 -4
- package/src/claude/rules/review-audit-self-decision.md +3 -3
- package/src/claude/rules/skill-domain-routing.md +1 -3
- package/src/claude/rules/skill-workflow-routing.md +4 -5
- package/src/claude/rules/state-sync.md +3 -12
- package/src/claude/rules/workflow.md +11 -19
- package/src/claude/runtime.schema.json +5 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
- package/src/claude/scripts/provenance.cjs +23 -5
- package/src/claude/scripts/spec-receipt.cjs +24 -121
- package/src/claude/scripts/spec-resolver.cjs +41 -458
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -32
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/{question → ask}/SKILL.md +9 -75
- package/src/claude/skills/brainstorm/SKILL.md +43 -131
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +29 -81
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
- package/src/claude/skills/debug/SKILL.md +29 -149
- package/src/claude/skills/develop/SKILL.md +26 -83
- package/src/claude/skills/develop/references/parallel-waves.md +0 -8
- package/src/claude/skills/develop/references/quality-gate.md +15 -13
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/docs/SKILL.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +1 -1
- package/src/claude/skills/fix/SKILL.md +153 -0
- package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
- package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
- package/src/claude/skills/git/SKILL.md +48 -9
- package/src/claude/skills/git/references/commit-protocols.md +35 -16
- package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
- package/src/claude/skills/orca/SKILL.md +9 -8
- package/src/claude/skills/research/SKILL.md +17 -46
- package/src/claude/skills/scout/SKILL.md +61 -0
- package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
- package/src/claude/skills/specs/SKILL.md +22 -29
- package/src/claude/skills/specs/references/review.md +8 -6
- package/src/claude/skills/specs/references/templates.md +34 -41
- package/src/claude/skills/sync/SKILL.md +16 -45
- package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
- package/src/claude/skills/sync/references/sync-protocols.md +4 -13
- package/src/claude/skills/test/SKILL.md +39 -104
- package/src/claude/skills/test/references/execution-strategy.md +93 -21
- package/src/claude/skills/test/references/failure-triage.md +1 -1
- package/src/claude/skills/ui-ux/LICENSE +21 -0
- package/src/claude/skills/ui-ux/SKILL.md +497 -0
- package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
- package/src/claude/skills/ui-ux/references/budgets.md +113 -0
- package/src/claude/skills/ui-ux/references/checklist.md +271 -0
- package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
- package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
- package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
- package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
- package/src/claude/skills/ui-ux/references/components/button.md +142 -0
- package/src/claude/skills/ui-ux/references/components/card.md +113 -0
- package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
- package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
- package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
- package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
- package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
- package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
- package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
- package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
- package/src/claude/skills/ui-ux/references/components/input.md +161 -0
- package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
- package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
- package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
- package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
- package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
- package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
- package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
- package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
- package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
- package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
- package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
- package/src/claude/skills/ui-ux/references/design-process.md +650 -0
- package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
- package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
- package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
- package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
- package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
- package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
- package/src/claude/skills/ui-ux/references/principles.md +408 -0
- package/src/claude/skills/ui-ux/references/refactor.md +245 -0
- package/src/claude/skills/ui-ux/references/responsive.md +195 -0
- package/src/claude/skills/ui-ux/references/review.md +528 -0
- package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
- package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
- package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
- package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
- package/src/claude/skills/ui-ux/references/styles.md +311 -0
- package/src/claude/skills/ui-ux/references/system.md +219 -0
- package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
- package/src/claude/skills/ui-ux/references/tokens.css +362 -0
- package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
- package/src/claude/skills/ui-ux/scripts/package.json +9 -0
- package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/claude/status.cjs +149 -144
- package/src/codex/AGENTS.md +8 -13
- package/src/codex/hooks/agent.cjs +2 -1
- package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
- package/src/codex/hooks/lib/spec-utils.cjs +6 -52
- package/src/codex/hooks/rules.cjs +2 -1
- package/src/codex/hooks/session.cjs +0 -2
- package/src/codex/hooks/spec-gate.cjs +13 -81
- package/src/codex/hooks/spec-state.cjs +37 -41
- package/src/codex/hooks.json +0 -20
- package/src/codex/rules/state-sync.md +3 -12
- package/src/common/AGENTS.md +6 -12
- package/src/omp/extensions/cafekit-bridge.mjs +6 -6
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
- package/src/claude/hooks/completion-authority-check.cjs +0 -56
- package/src/claude/hooks/completion-authority-state.cjs +0 -527
- package/src/claude/hooks/completion-authority.cjs +0 -163
- package/src/claude/hooks/lib/context.cjs +0 -616
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/hooks/semantic-review-authority.cjs +0 -154
- package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
- package/src/claude/hooks/usage.cjs +0 -197
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3941
- package/src/claude/skills/code-review/references/verification-gate.md +0 -46
- package/src/claude/skills/delegate/SKILL.md +0 -124
- package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
- package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
- package/src/claude/skills/hotfix/SKILL.md +0 -289
- package/src/claude/skills/inspect/SKILL.md +0 -142
- package/src/claude/skills/loop/SKILL.md +0 -114
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
- package/src/claude/skills/route/SKILL.md +0 -94
- package/src/claude/skills/route/references/agent-timing.md +0 -46
- package/src/claude/skills/route/references/chaining-patterns.md +0 -53
- package/src/claude/skills/route/references/task-taxonomy.md +0 -48
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- package/src/codex/hooks/completion-authority-check.cjs +0 -67
- package/src/codex/hooks/completion-authority-state.cjs +0 -527
- package/src/codex/hooks/completion-authority.cjs +0 -109
- package/src/codex/hooks/semantic-review-authority.cjs +0 -130
- package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
- /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
|
@@ -14,16 +14,9 @@ const POLICY = require(POLICY_PATH);
|
|
|
14
14
|
const PROVENANCE_HELPER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/provenance.cjs'));
|
|
15
15
|
const RESOLVER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
|
|
16
16
|
const RECEIPTS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-receipt.cjs'));
|
|
17
|
-
const FINAL_STATE = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-final-state.cjs'));
|
|
18
|
-
const VALIDATOR = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-spec-output.cjs'));
|
|
19
|
-
const GROUNDER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-ground.cjs'));
|
|
20
|
-
const SEMANTIC_AUTHORITY = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs'));
|
|
21
|
-
const READINESS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'));
|
|
22
|
-
const AUTHORING_VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
|
|
23
17
|
const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
|
|
24
18
|
const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
|
|
25
19
|
const SPECS = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md');
|
|
26
|
-
const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
|
|
27
20
|
const GATE = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/quality-gate.md');
|
|
28
21
|
const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
|
|
29
22
|
const TEST_SOURCE_ROOT = path.dirname(TEST_SKILL);
|
|
@@ -65,7 +58,6 @@ after(() => fs.rmSync(RUNTIME_ROOT, { recursive: true, force: true }));
|
|
|
65
58
|
const EXPECTED_BASE = RUNTIME_CONTEXT.base;
|
|
66
59
|
const EXPECTED_HEAD = RUNTIME_CONTEXT.head;
|
|
67
60
|
const EXPECTED_PROVENANCE = { base: EXPECTED_BASE, head: EXPECTED_HEAD };
|
|
68
|
-
const RECEIPT_BINDING = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
|
|
69
61
|
|
|
70
62
|
function initFixtureGit(root) {
|
|
71
63
|
for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['add', '-A'], ['commit', '-qm', 'fixture']]) {
|
|
@@ -80,9 +72,6 @@ function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs')
|
|
|
80
72
|
const hooksRoot = path.join(PACKAGE_ROOT, 'src', 'claude', 'hooks');
|
|
81
73
|
for (const hook of [
|
|
82
74
|
'spec-gate.cjs',
|
|
83
|
-
'completion-authority-check.cjs',
|
|
84
|
-
'completion-authority-state.cjs',
|
|
85
|
-
'semantic-review-authority.cjs',
|
|
86
75
|
]) {
|
|
87
76
|
const target = path.join(destinationRoot, 'hooks', hook);
|
|
88
77
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -163,182 +152,10 @@ function assertVocabularyIsLegacyOnly(filePath, regions) {
|
|
|
163
152
|
);
|
|
164
153
|
}
|
|
165
154
|
|
|
166
|
-
function runNode(root, script, args) {
|
|
167
|
-
return spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function commandOutput(result) {
|
|
171
|
-
return `${result.stdout}\n${result.stderr}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function createCanonicalAuthoringFixture() {
|
|
175
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-develop-contract-'));
|
|
176
|
-
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
177
|
-
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
178
|
-
fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
|
|
179
|
-
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { enabled: true };\n');
|
|
180
|
-
fs.writeFileSync(path.join(root, 'test', 'service.test.js'), 'require("node:test")("service", () => {});\n');
|
|
181
|
-
const scaffold = runNode(root, SCAFFOLD, ['feature']);
|
|
182
|
-
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
183
|
-
const specDir = path.join(root, 'specs', 'feature');
|
|
184
|
-
const specPath = path.join(specDir, 'spec.json');
|
|
185
|
-
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
186
|
-
|
|
187
|
-
## Requirements
|
|
188
|
-
### Requirement 1: Observable service behavior
|
|
189
|
-
- **R1.1**: When valid input arrives, the service shall return enabled state; invalid input shall return rejected state.
|
|
190
|
-
- **R1.2**: The verifier shall produce separate observable proof for enabled and rejected states.
|
|
191
|
-
`);
|
|
192
|
-
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
193
|
-
|
|
194
|
-
## Architecture
|
|
195
|
-
The entrypoint exposes the bounded service behavior and a separate proof artifact.
|
|
196
|
-
|
|
197
|
-
## Typed Anchors
|
|
198
|
-
| ID | Type | Target | Role | Access | Action |
|
|
199
|
-
|---|---|---|---|---|---|
|
|
200
|
-
| A-D-01 | file | \`src/entry.js\` | service boundary | read | read |
|
|
201
|
-
| A-D-02 | artifact | \`artifacts/design-proof.json\` | proof evidence | write | create |
|
|
202
|
-
|
|
203
|
-
## Canonical Contracts & Invariants
|
|
204
|
-
### D1 — Dispatch decision
|
|
205
|
-
The entrypoint dispatches exactly once.
|
|
206
|
-
### I1 — Invalid-state invariant
|
|
207
|
-
Invalid input never returns enabled state.
|
|
208
|
-
### C1 — Result contract
|
|
209
|
-
The result contains one enabled boolean.
|
|
210
|
-
|
|
211
|
-
## Verification Definitions
|
|
212
|
-
- **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/service.test.js\`; Expected exit 0 with enabled state and separate proof evidence; Negative/failure invalid input returns rejected state without enabled output; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01, A-D-02.
|
|
213
|
-
`);
|
|
214
|
-
return { root, specDir, specPath };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function createOrdinaryAuthoringFixture() {
|
|
218
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-ordinary-readiness-'));
|
|
219
|
-
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
220
|
-
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
221
|
-
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
222
|
-
fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), 'require("node:test")("entry", () => {});\n');
|
|
223
|
-
const scaffold = runNode(root, SCAFFOLD, ['ordinary']);
|
|
224
|
-
assert.equal(scaffold.status, 0, commandOutput(scaffold));
|
|
225
|
-
const specDir = path.join(root, 'specs', 'ordinary');
|
|
226
|
-
const specPath = path.join(specDir, 'spec.json');
|
|
227
|
-
fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
|
|
228
|
-
|
|
229
|
-
## Requirements
|
|
230
|
-
### Requirement 1: Delete behavior
|
|
231
|
-
- **R1.1**: When deletion is requested, the service shall delete the selected record and report deleted state.
|
|
232
|
-
`);
|
|
233
|
-
fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
|
|
234
|
-
|
|
235
|
-
## Typed Anchors
|
|
236
|
-
| ID | Type | Target | Role | Access | Action |
|
|
237
|
-
|---|---|---|---|---|---|
|
|
238
|
-
| A-D-01 | file | \`src/entry.js\` | behavior owner | read | read |
|
|
239
|
-
|
|
240
|
-
## Canonical Contracts & Invariants
|
|
241
|
-
### D1 — Delete decision
|
|
242
|
-
The selected record is deleted rather than archived.
|
|
243
|
-
### I1 — State invariant
|
|
244
|
-
Deleted state is reported only after deletion succeeds.
|
|
245
|
-
### C1 — Result contract
|
|
246
|
-
The result contains the literal deleted state.
|
|
247
|
-
|
|
248
|
-
## Verification Definitions
|
|
249
|
-
- **V1**: Criteria R1.1; Owner A-D-01; Decision refs D1, I1, C1; Method command \`node --test test/entry.test.js\`; Expected exit 0 and the service reports deleted state; Negative/failure a missing record returns not-found without deleted state; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01.
|
|
250
|
-
`);
|
|
251
|
-
const semantic = runNode(root, SCAFFOLD, ['ordinary', '--sync-semantic-model']);
|
|
252
|
-
assert.equal(semantic.status, 0, commandOutput(semantic));
|
|
253
|
-
const authoring = runNode(root, AUTHORING_VALIDATOR, [specDir]);
|
|
254
|
-
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
255
|
-
initFixtureGit(root);
|
|
256
|
-
return { root, specDir, specPath };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function ordinaryReview() {
|
|
260
|
-
return canonicalPassReview(['R1.1'], [{
|
|
261
|
-
criterion: 'R1.1', case_kind: 'failure',
|
|
262
|
-
scenario: 'Deletion targets a record that does not exist in the current store.',
|
|
263
|
-
expected: 'The service returns not-found and never reports the record as deleted.',
|
|
264
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
265
|
-
}]);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function canonicalPassReview(reviewedCriteria, counterexamples) {
|
|
269
|
-
return {
|
|
270
|
-
verdict: 'PASS',
|
|
271
|
-
findings: [],
|
|
272
|
-
unresolved_decisions: [],
|
|
273
|
-
graph_coverage: [
|
|
274
|
-
'criterion_local', 'cross_criterion', 'runtime_path',
|
|
275
|
-
'assumption_provenance', 'compatibility_migration',
|
|
276
|
-
].map((surface) => ({
|
|
277
|
-
surface, covered: true,
|
|
278
|
-
notes: 'The review covers this semantic surface against the canonical model.',
|
|
279
|
-
})),
|
|
280
|
-
reviewed_criteria: reviewedCriteria,
|
|
281
|
-
counterexamples,
|
|
282
|
-
reviewer_evidence: null,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function promoteCanonicalModel(fixture) {
|
|
287
|
-
const result = runNode(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
|
|
288
|
-
assert.equal(result.status, 0, commandOutput(result));
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function makeCanonicalReady(fixture) {
|
|
292
|
-
const authoring = runNode(fixture.root, AUTHORING_VALIDATOR, [fixture.specDir]);
|
|
293
|
-
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
294
|
-
const result = READINESS.finalizeReadiness({
|
|
295
|
-
specDir: fixture.specDir,
|
|
296
|
-
projectRoot: fixture.root,
|
|
297
|
-
reviewResult: canonicalPassReview(['R1.1', 'R1.2'], [{
|
|
298
|
-
criterion: 'R1.1', case_kind: 'failure',
|
|
299
|
-
scenario: 'The service receives invalid input without its required discriminator.',
|
|
300
|
-
expected: 'The service returns rejected state and never emits enabled true.',
|
|
301
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
302
|
-
}, {
|
|
303
|
-
criterion: 'R1.2', case_kind: 'adversarial',
|
|
304
|
-
scenario: 'The service reports enabled state without producing separate proof evidence.',
|
|
305
|
-
expected: 'Verification fails because the independent proof artifact is absent.',
|
|
306
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
307
|
-
}]),
|
|
308
|
-
});
|
|
309
|
-
return result.spec;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function bindFixtureReceipt(root, value, feature = 'demo', session = 'test') {
|
|
313
|
-
const context = PROVENANCE_HELPER.deriveRuntimeContext({
|
|
314
|
-
projectRoot: root,
|
|
315
|
-
specsRoot: path.join(root, 'specs'),
|
|
316
|
-
specFile: path.join(root, 'specs', feature, 'spec.json'),
|
|
317
|
-
featureName: feature,
|
|
318
|
-
runtimeSession: session,
|
|
319
|
-
});
|
|
320
|
-
return value.replaceAll('0123456789abcdef0123456789abcdef01234567', context.base)
|
|
321
|
-
.replaceAll('89abcdef0123456789abcdef0123456789abcdef', context.head);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
155
|
function canonicalReceipt(command = 'pnpm test', extra = '') {
|
|
325
156
|
return `Verification: PASS\nCommand: ${command}\nExit: 0\nBase: ${EXPECTED_BASE}\nHead: ${EXPECTED_HEAD}\n${extra}`;
|
|
326
157
|
}
|
|
327
158
|
|
|
328
|
-
function canonicalFlashTask(overrides = {}) {
|
|
329
|
-
return {
|
|
330
|
-
status: 'in_progress',
|
|
331
|
-
receipt: 'FLASH_UNVERIFIED',
|
|
332
|
-
blocker: 'awaiting /cf:test <feature>',
|
|
333
|
-
dependencyBlocked: true,
|
|
334
|
-
unblocks: false,
|
|
335
|
-
feature_name: 'demo',
|
|
336
|
-
runtime_context: RUNTIME_CONTEXT,
|
|
337
|
-
expected_provenance: { ...EXPECTED_PROVENANCE },
|
|
338
|
-
...overrides,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
159
|
function read(filePath) {
|
|
343
160
|
return fs.readFileSync(filePath, 'utf8');
|
|
344
161
|
}
|
|
@@ -381,388 +198,22 @@ test('develop flags retain their process-v3 contracts', () => {
|
|
|
381
198
|
assert.match(develop, /FLASH_UNVERIFIED[\s\S]*do not unblock dependents[\s\S]*sync-finalize path/i);
|
|
382
199
|
});
|
|
383
200
|
|
|
384
|
-
test('
|
|
385
|
-
const light = POLICY.delegationPlan({ tier: 'Light', mode: 'full-spec', taskCount: 2 });
|
|
386
|
-
const standard = POLICY.delegationPlan({ tier: 'Standard', mode: 'specific-task' });
|
|
387
|
-
const deep = POLICY.delegationPlan({ tier: 'Deep', mode: 'full-spec', taskCount: 2 });
|
|
388
|
-
assert.deepEqual(light.proof_obligations, ['needsExecutionProof']);
|
|
389
|
-
assert.deepEqual(standard.proof_obligations, ['needsExecutionProof']);
|
|
390
|
-
assert.deepEqual(deep.proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
391
|
-
for (const plan of [light, standard, deep]) {
|
|
392
|
-
assert.equal(Object.hasOwn(plan, 'delegated'), false);
|
|
393
|
-
assert.doesNotMatch(JSON.stringify(plan), /inspector|implementer|test-runner|code-auditor/);
|
|
394
|
-
}
|
|
201
|
+
test('review and sync skills keep the verdict vocabulary and the sync-finalize path', () => {
|
|
395
202
|
assert.doesNotMatch(read(GATE), /spec-review|quality-review/);
|
|
396
203
|
assert.doesNotMatch(read(DEVELOP), /spec-review|quality-review/);
|
|
204
|
+
assert.match(read(GATE), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
205
|
+
assert.match(read(CODE_REVIEW_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
206
|
+
assert.match(read(TEST_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
207
|
+
assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/i);
|
|
208
|
+
assert.match(read(SYNC_SKILL), /sync-finalize/);
|
|
397
209
|
});
|
|
398
210
|
|
|
399
|
-
test('
|
|
400
|
-
const result = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
|
|
401
|
-
assert.equal(result.lane, 'Direct');
|
|
402
|
-
assert.equal(result.automaticLane, 'Direct');
|
|
403
|
-
assert.deepEqual(result.risks, []);
|
|
404
|
-
const policy = POLICY.lanePolicy(result);
|
|
405
|
-
assert.deepEqual(policy.proof_obligations, ['needsExecutionProof']);
|
|
406
|
-
assert.equal(policy.artifact_profile, 'targeted');
|
|
407
|
-
assert.equal(policy.requiresSpec, false);
|
|
408
|
-
assert.equal(policy.requiresState, false);
|
|
409
|
-
assert.equal(policy.proof_obligations.includes('needsDurableTaskState'), false);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test('lane classifier raises named risks to Elevated without inventing independent-audit authority', () => {
|
|
413
|
-
const result = POLICY.classifyLane({
|
|
414
|
-
reversible: true,
|
|
415
|
-
riskSignals: { auth: true, migration: true, publicContract: true },
|
|
416
|
-
});
|
|
417
|
-
assert.equal(result.lane, 'Standard');
|
|
418
|
-
assert.equal(result.assuranceLevel, 'Elevated');
|
|
419
|
-
assert.equal(result.automaticAssuranceLevel, 'Elevated');
|
|
420
|
-
assert.deepEqual(result.risks, ['auth', 'migration', 'publicContract']);
|
|
421
|
-
assert.deepEqual(POLICY.lanePolicy(result).proof_obligations, [
|
|
422
|
-
'needsInspection', 'needsExecutionProof',
|
|
423
|
-
]);
|
|
424
|
-
|
|
425
|
-
const strict = POLICY.classifyLane({
|
|
426
|
-
riskSignals: { auth: true },
|
|
427
|
-
assurance_level: 'Strict',
|
|
428
|
-
});
|
|
429
|
-
assert.equal(strict.lane, 'Critical');
|
|
430
|
-
assert.equal(strict.automaticLane, 'Standard');
|
|
431
|
-
assert.equal(strict.assuranceLevel, 'Strict');
|
|
432
|
-
assert.equal(strict.automaticAssuranceLevel, 'Elevated');
|
|
433
|
-
assert.ok(POLICY.lanePolicy(strict).proof_obligations.includes('needsIndependentAudit'));
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test('P1 persists the minimal v2.1 policy and derives compatibility views', () => {
|
|
437
|
-
const initial = POLICY.persistWorkflowPolicySnapshot(
|
|
438
|
-
{ feature_name: 'bounded' },
|
|
439
|
-
{ riskSignals: {} },
|
|
440
|
-
);
|
|
441
|
-
const snapshot = initial.workflow_policy;
|
|
442
|
-
assert.deepEqual(Object.keys(snapshot).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
443
|
-
assert.equal(snapshot.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
444
|
-
assert.equal(snapshot.planning_depth, 'Compact');
|
|
445
|
-
assert.equal(snapshot.assurance_level, 'Routine');
|
|
446
|
-
assert.deepEqual(snapshot.classified_minimum, {
|
|
447
|
-
planning_depth: 'Compact',
|
|
448
|
-
assurance_level: 'Routine',
|
|
449
|
-
});
|
|
450
|
-
for (const derivedField of ['lane', 'automatic_lane', 'artifact_profile', 'proof_obligations', 'actor_needs']) {
|
|
451
|
-
assert.equal(Object.hasOwn(snapshot, derivedField), false, `${derivedField} must not be persisted`);
|
|
452
|
-
}
|
|
453
|
-
const view = POLICY.readWorkflowPolicySnapshot(initial);
|
|
454
|
-
assert.equal(view.lane, 'Standard');
|
|
455
|
-
assert.equal(view.automatic_lane, 'Standard');
|
|
456
|
-
assert.equal(view.artifact_profile, 'bounded');
|
|
457
|
-
assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
|
|
458
|
-
assert.equal(Object.hasOwn(initial, 'override_' + 'receipt'), false);
|
|
459
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(snapshot).valid, true);
|
|
460
|
-
|
|
461
|
-
const persistedAgain = POLICY.persistWorkflowPolicySnapshot(initial, {
|
|
462
|
-
riskSignals: { auth: true },
|
|
463
|
-
override: 'Direct',
|
|
464
|
-
});
|
|
465
|
-
assert.deepEqual(persistedAgain.workflow_policy, snapshot, 'persist-once must not reclassify an existing snapshot');
|
|
466
|
-
|
|
467
|
-
const malformed = { ...snapshot, lane: 'Critical' };
|
|
468
|
-
const validation = POLICY.validateWorkflowPolicySnapshot(malformed);
|
|
469
|
-
assert.equal(validation.valid, false);
|
|
470
|
-
assert.match(validation.errors.join('; '), /v2\.1 fields must be exactly/);
|
|
471
|
-
assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: malformed }), /Invalid workflow policy snapshot/);
|
|
472
|
-
|
|
473
|
-
const malformedShape = { ...snapshot, classified_minimum: { planning_depth: 'Compact' } };
|
|
474
|
-
const shapeValidation = POLICY.validateWorkflowPolicySnapshot(malformedShape);
|
|
475
|
-
assert.equal(shapeValidation.valid, false);
|
|
476
|
-
assert.match(shapeValidation.errors.join('; '), /classified_minimum must contain exactly/);
|
|
477
|
-
|
|
478
|
-
const malformedRisks = { ...snapshot, risks: { auth: true } };
|
|
479
|
-
const risksValidation = POLICY.validateWorkflowPolicySnapshot(malformedRisks);
|
|
480
|
-
assert.equal(risksValidation.valid, false);
|
|
481
|
-
assert.match(risksValidation.errors.join('; '), /risks must be an array/);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
test('P1 workflow-policy validation is semantic and explicit workflow_policy values never reclassify', () => {
|
|
485
|
-
const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
486
|
-
const standardUnknown = POLICY.escalateWorkflowPolicy(direct, { risks: ['unclassified-risk'] });
|
|
487
|
-
const elevatedAuth = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
|
|
488
|
-
const escalatedElevated = POLICY.escalateWorkflowPolicy(direct, { risks: ['auth'] });
|
|
489
|
-
|
|
490
|
-
for (const legitimate of [direct, standardUnknown, elevatedAuth, escalatedElevated]) {
|
|
491
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(legitimate).valid, true);
|
|
492
|
-
}
|
|
493
|
-
assert.equal(escalatedElevated.lane, 'Standard');
|
|
494
|
-
assert.equal(escalatedElevated.automatic_lane, 'Standard');
|
|
495
|
-
assert.equal(escalatedElevated.planning_depth, 'None');
|
|
496
|
-
assert.equal(escalatedElevated.assurance_level, 'Elevated');
|
|
497
|
-
|
|
498
|
-
const forgedDirectAuth = { ...direct, risks: ['auth'] };
|
|
499
|
-
const forgedDirectUnknown = { ...direct, risks: ['unclassified-risk'] };
|
|
500
|
-
const forgedStandardAuth = { ...standardUnknown, risks: ['auth'] };
|
|
501
|
-
for (const forged of [forgedDirectAuth, forgedDirectUnknown]) {
|
|
502
|
-
const result = POLICY.validateWorkflowPolicySnapshot(forged);
|
|
503
|
-
assert.equal(result.valid, false);
|
|
504
|
-
assert.match(result.errors.join('; '), /at least (Elevated|Strict)/);
|
|
505
|
-
}
|
|
506
|
-
assert.equal(
|
|
507
|
-
POLICY.validateWorkflowPolicySnapshot(forgedStandardAuth).valid,
|
|
508
|
-
true,
|
|
509
|
-
'changing one normalized risk to another does not invent a higher assurance minimum',
|
|
510
|
-
);
|
|
511
|
-
for (const riskAlias of ['Auth', 'authentication', 'public_contract']) {
|
|
512
|
-
const escalated = POLICY.escalateWorkflowPolicy(direct, { risks: [riskAlias] });
|
|
513
|
-
assert.equal(escalated.lane, 'Standard', `${riskAlias} must classify as Elevated`);
|
|
514
|
-
assert.equal(escalated.assurance_level, 'Elevated');
|
|
515
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot({ ...direct, risks: [riskAlias] }).valid, false);
|
|
516
|
-
}
|
|
517
|
-
assert.throws(() => POLICY.assertWorkflowPolicySnapshot(forgedDirectAuth), /Invalid workflow policy snapshot/);
|
|
518
|
-
assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
|
|
519
|
-
assert.throws(() => POLICY.lanePolicy({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
|
|
520
|
-
|
|
521
|
-
for (const explicitValue of [null, undefined]) {
|
|
522
|
-
const state = { workflow_policy: explicitValue, reversible: true, lowRisk: true, isolated: true };
|
|
523
|
-
assert.throws(() => POLICY.workflowPolicySnapshot(state), /Invalid workflow policy snapshot/);
|
|
524
|
-
assert.throws(() => POLICY.escalateWorkflowPolicy(state, {}), /Invalid workflow policy snapshot/);
|
|
525
|
-
assert.throws(() => POLICY.lanePolicy(state), /Invalid workflow policy snapshot/);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const forgedLanePolicyCli = spawnSync(process.execPath, [
|
|
529
|
-
POLICY_PATH,
|
|
530
|
-
'--lane-policy',
|
|
531
|
-
'--task-json',
|
|
532
|
-
JSON.stringify({ workflow_policy: forgedDirectAuth }),
|
|
533
|
-
'--json',
|
|
534
|
-
], { encoding: 'utf8' });
|
|
535
|
-
assert.equal(forgedLanePolicyCli.status, 2, `${forgedLanePolicyCli.stdout}\n${forgedLanePolicyCli.stderr}`);
|
|
536
|
-
assert.match(forgedLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
|
|
537
|
-
|
|
538
|
-
const nullLanePolicyCli = spawnSync(process.execPath, [
|
|
539
|
-
POLICY_PATH,
|
|
540
|
-
'--lane-policy',
|
|
541
|
-
'--task-json',
|
|
542
|
-
JSON.stringify({ workflow_policy: null, reversible: true, lowRisk: true, isolated: true }),
|
|
543
|
-
'--json',
|
|
544
|
-
], { encoding: 'utf8' });
|
|
545
|
-
assert.equal(nullLanePolicyCli.status, 2, `${nullLanePolicyCli.stdout}\n${nullLanePolicyCli.stderr}`);
|
|
546
|
-
assert.match(nullLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
|
|
547
|
-
|
|
548
|
-
const validatePolicyCli = spawnSync(process.execPath, [
|
|
549
|
-
POLICY_PATH,
|
|
550
|
-
'--validate-policy',
|
|
551
|
-
'--task-json',
|
|
552
|
-
JSON.stringify({ workflow_policy: forgedDirectAuth }),
|
|
553
|
-
'--json',
|
|
554
|
-
], { encoding: 'utf8' });
|
|
555
|
-
assert.equal(validatePolicyCli.status, 2, `${validatePolicyCli.stdout}\n${validatePolicyCli.stderr}`);
|
|
556
|
-
assert.equal(JSON.parse(validatePolicyCli.stdout).valid, false);
|
|
557
|
-
|
|
558
|
-
const validateNullPolicyCli = spawnSync(process.execPath, [
|
|
559
|
-
POLICY_PATH,
|
|
560
|
-
'--validate-policy',
|
|
561
|
-
'--task-json',
|
|
562
|
-
JSON.stringify({ workflow_policy: null }),
|
|
563
|
-
'--json',
|
|
564
|
-
], { encoding: 'utf8' });
|
|
565
|
-
assert.equal(validateNullPolicyCli.status, 2, `${validateNullPolicyCli.stdout}\n${validateNullPolicyCli.stderr}`);
|
|
566
|
-
assert.equal(JSON.parse(validateNullPolicyCli.stdout).valid, false);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test('P1 workflow policy escalation is monotonic across all newly classified risks', () => {
|
|
570
|
-
const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
571
|
-
assert.equal(direct.lane, 'Direct');
|
|
572
|
-
assert.deepEqual(POLICY.escalateWorkflowPolicy(direct, {}), direct, 'no new risks must not escalate');
|
|
573
|
-
|
|
574
|
-
const standard = POLICY.escalateWorkflowPolicy(direct, { risks: ['new-standard-risk'], riskLevel: 'standard' });
|
|
575
|
-
assert.equal(standard.lane, 'Standard');
|
|
576
|
-
assert.equal(standard.artifact_profile, 'targeted');
|
|
577
|
-
assert.equal(standard.planning_depth, 'None');
|
|
578
|
-
assert.equal(standard.assurance_level, 'Elevated');
|
|
579
|
-
assert.equal(standard.risks.includes('new-standard-risk'), true);
|
|
580
|
-
assert.deepEqual(standard.proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
581
|
-
|
|
582
|
-
const ambiguityElevated = POLICY.escalateWorkflowPolicy(direct, { riskSignals: { ambiguity: true } });
|
|
583
|
-
assert.equal(ambiguityElevated.lane, 'Standard', 'risk discovery must require inspection without inventing an audit');
|
|
584
|
-
assert.equal(ambiguityElevated.assurance_level, 'Elevated');
|
|
585
|
-
|
|
586
|
-
const criticalSeverity = POLICY.escalateWorkflowPolicy(direct, { risks: ['payment'], riskLevel: 'critical' });
|
|
587
|
-
assert.equal(criticalSeverity.assurance_level, 'Elevated', 'severity labels must not create independent-audit authority');
|
|
588
|
-
assert.equal(criticalSeverity.proof_obligations.includes('needsIndependentAudit'), false);
|
|
589
|
-
|
|
590
|
-
const critical = POLICY.escalateWorkflowPolicy(standard, { risks: ['auth'], assurance_level: 'Strict' });
|
|
591
|
-
assert.equal(critical.lane, 'Critical');
|
|
592
|
-
assert.equal(critical.artifact_profile, 'targeted');
|
|
593
|
-
assert.equal(critical.planning_depth, 'None');
|
|
594
|
-
assert.equal(critical.assurance_level, 'Strict');
|
|
595
|
-
assert.equal(critical.automatic_assurance_level, 'Elevated');
|
|
596
|
-
assert.equal(critical.proof_obligations.includes('needsIndependentAudit'), true);
|
|
597
|
-
assert.equal(critical.proof_obligations.includes('needsResearchGrounding'), false);
|
|
598
|
-
|
|
599
|
-
const noDowngrade = POLICY.escalateWorkflowPolicy(critical, { risks: ['another-standard-risk'], riskLevel: 'standard' });
|
|
600
|
-
assert.equal(noDowngrade.lane, 'Critical');
|
|
601
|
-
assert.equal(noDowngrade.proof_obligations.includes('needsIndependentAudit'), true);
|
|
602
|
-
assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'), false);
|
|
603
|
-
|
|
604
|
-
const criticalAgain = POLICY.escalateWorkflowPolicy(critical, { risks: ['auth'] });
|
|
605
|
-
assert.deepEqual(criticalAgain, critical, 'repeating known risks must not create a new escalation');
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
test('P1 legacy execution tier is read-compatible without becoming emitted policy authority', () => {
|
|
609
|
-
const legacy = { design_context: { execution_tier: 'Deep' } };
|
|
610
|
-
const snapshot = POLICY.workflowPolicySnapshot(legacy);
|
|
611
|
-
assert.equal(snapshot.lane, 'Critical');
|
|
612
|
-
assert.equal(snapshot.artifact_profile, 'strict');
|
|
613
|
-
const policy = POLICY.lanePolicy(legacy);
|
|
614
|
-
assert.equal(policy.lane, 'Critical');
|
|
615
|
-
assert.equal(Object.hasOwn(policy, 'execution_tier'), false);
|
|
616
|
-
assert.equal(Object.hasOwn(policy, 'executionTier'), false);
|
|
617
|
-
const legacyPlan = POLICY.delegationPlan({ tier: 'Deep' });
|
|
618
|
-
assert.equal(Object.hasOwn(legacyPlan, 'execution_tier'), false);
|
|
619
|
-
assert.equal(Object.hasOwn(legacyPlan, 'executionTier'), false);
|
|
620
|
-
assert.deepEqual(legacyPlan.proof_obligations, snapshot.proof_obligations);
|
|
621
|
-
assert.equal(POLICY.workflowPolicySnapshot({ design_context: { execution_tier: 'standard' } }).lane, 'Standard');
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
test('legacy v1 policy is read-compatible, immutable on read, and explicitly migrates to v2.1', () => {
|
|
625
|
-
const cases = [
|
|
626
|
-
{ lane: 'Direct', risks: [], obligations: ['needsExecutionProof'] },
|
|
627
|
-
{ lane: 'Standard', risks: [], obligations: ['needsInspection', 'needsExecutionProof'] },
|
|
628
|
-
{ lane: 'Critical', risks: ['auth'], obligations: ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding'] },
|
|
629
|
-
];
|
|
630
|
-
for (const fixture of cases) {
|
|
631
|
-
const v1 = {
|
|
632
|
-
version: '1',
|
|
633
|
-
lane: fixture.lane,
|
|
634
|
-
automatic_lane: fixture.lane,
|
|
635
|
-
risks: fixture.risks,
|
|
636
|
-
artifact_profile: { Direct: 'targeted', Standard: 'bounded', Critical: 'strict' }[fixture.lane],
|
|
637
|
-
proof_obligations: fixture.obligations,
|
|
638
|
-
actor_needs: POLICY.actorNeedsFor(fixture.obligations),
|
|
639
|
-
override_receipt: null,
|
|
640
|
-
};
|
|
641
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(v1).valid, true, fixture.lane);
|
|
642
|
-
const source = { feature_name: `legacy-${fixture.lane}`, workflow_policy: v1 };
|
|
643
|
-
const before = JSON.stringify(source);
|
|
644
|
-
const adapted = POLICY.readWorkflowPolicySnapshot(source);
|
|
645
|
-
assert.equal(adapted.version, '2');
|
|
646
|
-
assert.deepEqual(adapted.proof_obligations, fixture.obligations, fixture.lane);
|
|
647
|
-
assert.deepEqual(adapted.actor_needs, v1.actor_needs, fixture.lane);
|
|
648
|
-
assert.equal(POLICY.validateWorkflowPolicySnapshot(adapted).valid, true, fixture.lane);
|
|
649
|
-
assert.equal(JSON.stringify(source), before, 'read adapter must not mutate persisted v1 state');
|
|
650
|
-
const migrated = POLICY.persistWorkflowPolicySnapshot(source).workflow_policy;
|
|
651
|
-
assert.equal(migrated.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
652
|
-
assert.deepEqual(Object.keys(migrated).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
|
|
653
|
-
const migratedView = POLICY.readWorkflowPolicySnapshot({ workflow_policy: migrated });
|
|
654
|
-
assert.equal(migratedView.lane, adapted.lane, fixture.lane);
|
|
655
|
-
assert.equal(migratedView.artifact_profile, adapted.artifact_profile, fixture.lane);
|
|
656
|
-
assert.deepEqual(migratedView.proof_obligations, POLICY.obligationsForAssurance(
|
|
657
|
-
migrated.assurance_level,
|
|
658
|
-
migrated.risks,
|
|
659
|
-
), fixture.lane);
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
test('post-classification downgrade is unsupported', () => {
|
|
664
|
-
assert.throws(
|
|
665
|
-
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
666
|
-
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
667
|
-
);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
test('caller-requested downgrade is blocked before persistence', () => {
|
|
671
|
-
assert.throws(
|
|
672
|
-
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
|
|
673
|
-
(error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
674
|
-
);
|
|
675
|
-
// also via CLI
|
|
676
|
-
const cli = spawnSync(process.execPath, [
|
|
677
|
-
POLICY_PATH,
|
|
678
|
-
'--classify-lane',
|
|
679
|
-
'--task-json',
|
|
680
|
-
JSON.stringify({ riskSignals: { auth: true }, override: 'Direct' }),
|
|
681
|
-
'--json',
|
|
682
|
-
], { encoding: 'utf8' });
|
|
683
|
-
assert.equal(cli.status, 2);
|
|
684
|
-
assert.match(cli.stderr, /planning_depth downgrade.*not permitted/i);
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
test('forged approval via legacy approved field is rejected', () => {
|
|
688
|
-
assert.throws(() => POLICY.approvalState({ generated: true, approved: true }), /Legacy approval state/);
|
|
689
|
-
const cli = spawnSync(process.execPath, [
|
|
690
|
-
POLICY_PATH,
|
|
691
|
-
'--approval-state',
|
|
692
|
-
'--task-json',
|
|
693
|
-
JSON.stringify({ generated: true, approved: true }),
|
|
694
|
-
'--json',
|
|
695
|
-
], { encoding: 'utf8' });
|
|
696
|
-
assert.equal(cli.status, 2);
|
|
697
|
-
assert.match(cli.stderr, /Legacy approval state/);
|
|
698
|
-
const legacyCheck = POLICY.validateApprovalSchema({ approvals: { requirements: { generated: true, approved: true } } });
|
|
699
|
-
assert.equal(legacyCheck.valid, false);
|
|
700
|
-
assert.equal(legacyCheck.legacy, true);
|
|
701
|
-
assert.match(legacyCheck.error, /Legacy/);
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
test('technical approval readiness ignores legacy user_approved', () => {
|
|
705
|
-
const state = POLICY.approvalState({ generated: true, agent_validated: true });
|
|
706
|
-
assert.equal(state.generated, true);
|
|
707
|
-
assert.equal(state.agent_validated, true);
|
|
708
|
-
assert.equal(state.ready, true);
|
|
709
|
-
assert.equal(state.schema_version, '2.0');
|
|
710
|
-
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true }).ready, true);
|
|
711
|
-
assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false }).ready, true);
|
|
712
|
-
const cli = spawnSync(process.execPath, [
|
|
713
|
-
POLICY_PATH,
|
|
714
|
-
'--approval-state',
|
|
715
|
-
'--task-json',
|
|
716
|
-
JSON.stringify({ generated: true, agent_validated: true }),
|
|
717
|
-
'--json',
|
|
718
|
-
], { encoding: 'utf8' });
|
|
719
|
-
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
720
|
-
assert.equal(JSON.parse(cli.stdout).state.ready, true);
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
test('approval schema fails closed for explicit null, array, empty, and malformed states', () => {
|
|
724
|
-
const absent = POLICY.validateApprovalSchema({});
|
|
725
|
-
assert.equal(absent.valid, true);
|
|
726
|
-
assert.equal(absent.absent, true);
|
|
727
|
-
assert.equal(POLICY.approvalState({}).present, false);
|
|
728
|
-
|
|
729
|
-
const invalidSpecs = [
|
|
730
|
-
[],
|
|
731
|
-
{ approvals: null },
|
|
732
|
-
{ approvals: [] },
|
|
733
|
-
{ approvals: {} },
|
|
734
|
-
{ approvals: { requirements: null } },
|
|
735
|
-
{ approvals: { requirements: { generated: true, agent_validated: true, user_approved: null } } },
|
|
736
|
-
{ approvals: { requirements: { generated: true, agent_validated: true, user_approved: true, extra: false } } },
|
|
737
|
-
];
|
|
738
|
-
for (const spec of invalidSpecs) {
|
|
739
|
-
assert.equal(POLICY.validateApprovalSchema(spec).valid, false, `invalid approval should fail: ${JSON.stringify(spec)}`);
|
|
740
|
-
assert.throws(() => POLICY.approvalState(spec), /approval|approvals|schema/i);
|
|
741
|
-
}
|
|
742
|
-
assert.throws(() => POLICY.approvalState({ schema_version: null }), /schema_version/);
|
|
743
|
-
assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
test('CLI exposes a derived lane without making it primary Develop authority', () => {
|
|
747
|
-
const cli = spawnSync(process.execPath, [
|
|
748
|
-
POLICY_PATH,
|
|
749
|
-
'--classify-lane',
|
|
750
|
-
'--task-json',
|
|
751
|
-
JSON.stringify({ reversible: true, lowRisk: true, isolated: true }),
|
|
752
|
-
'--json',
|
|
753
|
-
], { encoding: 'utf8' });
|
|
754
|
-
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
755
|
-
const payload = JSON.parse(cli.stdout);
|
|
756
|
-
assert.equal(payload.classification.lane, 'Direct');
|
|
757
|
-
assert.equal(payload.policy.requiresSpec, false);
|
|
211
|
+
test('Develop names the process-first packet as its only authority', () => {
|
|
758
212
|
const develop = read(DEVELOP);
|
|
759
213
|
const regions = markdownLegacyRegions(develop);
|
|
760
214
|
assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
|
|
761
215
|
assert.doesNotMatch(regions.primary, /planning_depth|assurance_level|execution_tier|\blane\b/i);
|
|
762
|
-
assert.
|
|
763
|
-
assert.match(regions.legacy, /assurance_level/);
|
|
764
|
-
assert.match(regions.legacy, /execution_tier/);
|
|
765
|
-
assert.match(regions.legacy, /derived lane/i);
|
|
216
|
+
assert.doesNotMatch(develop, /## Legacy workflow compatibility|Also supports existing legacy Specs packets/);
|
|
766
217
|
assert.match(regions.primary, /specs\/<feature>\/plan\.md[\s\S]*task-NN-\*\.md/i);
|
|
767
218
|
assert.match(regions.primary, /one unblocked task at a time/i);
|
|
768
219
|
assert.match(regions.primary, /Verification Plan/);
|
|
@@ -771,84 +222,69 @@ test('CLI exposes a derived lane without making it primary Develop authority', (
|
|
|
771
222
|
assert.match(develop, /--notes.*opt-in/i);
|
|
772
223
|
assert.doesNotMatch(develop, /--no-notes/);
|
|
773
224
|
assert.doesNotMatch(develop, /planner|implementer|test-runner|code-auditor|docs-keeper/i);
|
|
774
|
-
const fullRoutine = POLICY.readWorkflowPolicySnapshot({
|
|
775
|
-
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Routine' }),
|
|
776
|
-
});
|
|
777
|
-
const compactStrict = POLICY.readWorkflowPolicySnapshot({
|
|
778
|
-
workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' }),
|
|
779
|
-
});
|
|
780
|
-
assert.equal(fullRoutine.planning_depth, 'Full');
|
|
781
|
-
assert.equal(fullRoutine.assurance_level, 'Routine');
|
|
782
|
-
assert.equal(compactStrict.planning_depth, 'Compact');
|
|
783
|
-
assert.equal(compactStrict.assurance_level, 'Strict');
|
|
784
|
-
assert.notEqual(fullRoutine.artifact_profile, compactStrict.artifact_profile);
|
|
785
|
-
assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
|
|
786
225
|
});
|
|
787
226
|
|
|
788
227
|
test('Specs primary output is a flat process-first packet with isolated legacy compatibility', () => {
|
|
789
228
|
const specs = read(SPECS);
|
|
790
229
|
const regions = markdownLegacyRegions(specs);
|
|
791
230
|
const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
|
|
792
|
-
assert.equal(legacyHeadings.length,
|
|
231
|
+
assert.equal(legacyHeadings.length, 0);
|
|
793
232
|
assert.match(regions.primary, /specs\/<feature>\/[\s\S]*plan\.md[\s\S]*task-01-<slug>\.md[\s\S]*task-02-<slug>\.md/i);
|
|
794
233
|
assert.match(regions.primary, /Task files are flat beside `plan\.md`/i);
|
|
795
234
|
assert.match(regions.primary, /one task at a time/i);
|
|
796
235
|
assert.match(regions.primary, /inline `## Receipt`/i);
|
|
797
|
-
assert.match(regions.primary, /
|
|
798
|
-
assert.
|
|
799
|
-
assert.match(regions.legacy, /never requires the legacy kernel/i);
|
|
236
|
+
assert.match(regions.primary, /GATE-SCOPE[\s\S]*GATE-REVIEW[\s\S]*GATE-DONE/i);
|
|
237
|
+
assert.doesNotMatch(specs, /installed legacy adapters?/i);
|
|
800
238
|
assertVocabularyIsLegacyOnly(SPECS, regions);
|
|
801
239
|
});
|
|
802
240
|
|
|
803
|
-
test('core execution agents default to process-first state and
|
|
241
|
+
test('core execution agents default to process-first state and carry no legacy adapter', () => {
|
|
804
242
|
for (const filePath of PROCESS_FIRST_AGENTS) {
|
|
805
243
|
const content = read(filePath);
|
|
806
244
|
const name = path.basename(filePath);
|
|
807
245
|
assert.match(content, /plan\.md/i, `${name} must read the process-first plan`);
|
|
808
246
|
assert.match(content, /task-NN-\*\.md|task-NN-<slug>\.md/i, `${name} must understand flat tasks`);
|
|
809
|
-
assert.
|
|
247
|
+
assert.doesNotMatch(content, /legacy|spec\.json|scope_lock/i, `${name} must not teach a legacy adapter`);
|
|
810
248
|
}
|
|
811
249
|
|
|
812
250
|
const implementer = read(PROCESS_FIRST_AGENTS[0]);
|
|
813
251
|
assert.match(implementer, /Under every dispatch mode[\s\S]{0,160}do NOT edit[\s\S]{0,80}`plan\.md`[\s\S]{0,80}`Status:`[\s\S]{0,80}`## Receipt`/i);
|
|
814
252
|
assert.match(implementer, /controller/i);
|
|
815
|
-
assert.match(implementer, /process-first work[\s\S]{0,100}Ownership
|
|
253
|
+
assert.match(implementer, /process-first work[\s\S]{0,100}Ownership\s+boundary\./i);
|
|
816
254
|
|
|
817
255
|
const auditor = read(PROCESS_FIRST_AGENTS[2]);
|
|
818
|
-
assert.match(auditor, /accepted
|
|
819
|
-
assert.
|
|
820
|
-
assert.match(auditor, /Never require those legacy artifacts from a[\s\S]*process-first packet/i);
|
|
821
|
-
assert.match(auditor, /attestation belongs only to the valid legacy/i);
|
|
256
|
+
assert.match(auditor, /accepted GATE-SCOPE\/GATE-REVIEW decisions/i);
|
|
257
|
+
assert.doesNotMatch(auditor, /CAFEKIT_SEMANTIC_REVIEW_ATTESTATION/);
|
|
822
258
|
|
|
823
259
|
const docsKeeper = read(PROCESS_FIRST_AGENTS[3]);
|
|
824
260
|
assert.match(docsKeeper, /never[\s\S]{0,80}invent or write Status, Receipt, approval, or execution proof/i);
|
|
825
261
|
|
|
826
262
|
const deployer = read(PROCESS_FIRST_AGENTS[4]);
|
|
827
263
|
assert.match(deployer, /current final inline Receipt/i);
|
|
828
|
-
assert.match(deployer, /
|
|
264
|
+
assert.match(deployer, /GATE-DONE\/release authorization/i);
|
|
829
265
|
assert.match(deployer, /does not write process-first Status\/Receipt/i);
|
|
830
266
|
});
|
|
831
267
|
|
|
832
268
|
test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Legacy sections', () => {
|
|
833
269
|
const surfaces = [
|
|
834
|
-
['develop', DEVELOP],
|
|
835
|
-
['parallel waves', PARALLEL_WAVES],
|
|
836
|
-
['sync', SYNC_SKILL],
|
|
837
|
-
['sync protocols', SYNC_PROTOCOLS],
|
|
270
|
+
['develop', DEVELOP, 0],
|
|
271
|
+
['parallel waves', PARALLEL_WAVES, 0],
|
|
272
|
+
['sync', SYNC_SKILL, 0],
|
|
273
|
+
['sync protocols', SYNC_PROTOCOLS, 0],
|
|
838
274
|
];
|
|
839
275
|
const regionsByName = new Map();
|
|
840
|
-
let legacyCorpus = '';
|
|
841
276
|
|
|
842
|
-
for (const [name, filePath] of surfaces) {
|
|
277
|
+
for (const [name, filePath, expectedLegacy] of surfaces) {
|
|
843
278
|
const regions = markdownLegacyRegions(read(filePath));
|
|
844
279
|
const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
|
|
845
|
-
assert.equal(legacyHeadings.length,
|
|
280
|
+
assert.equal(legacyHeadings.length, expectedLegacy, `${name} must have exactly ${expectedLegacy} Legacy heading(s)`);
|
|
846
281
|
assert.match(regions.primary, /inline (?:`## )?Receipts?/i, `${name} must teach inline Receipts`);
|
|
847
|
-
|
|
848
|
-
|
|
282
|
+
if (expectedLegacy === 1) {
|
|
283
|
+
assert.match(regions.legacy, /spec\.json/i, `${name} must retain the spec.json adapter`);
|
|
284
|
+
assert.match(regions.legacy, /task_registry/i, `${name} must retain task_registry compatibility`);
|
|
285
|
+
}
|
|
849
286
|
assertVocabularyIsLegacyOnly(filePath, regions);
|
|
850
287
|
regionsByName.set(name, regions);
|
|
851
|
-
legacyCorpus += `\n${regions.legacy}`;
|
|
852
288
|
}
|
|
853
289
|
|
|
854
290
|
const develop = regionsByName.get('develop').primary;
|
|
@@ -874,7 +310,7 @@ test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Leg
|
|
|
874
310
|
assert.match(protocols, /`plan\.md` and flat `task-\*\.md` files/i);
|
|
875
311
|
assert.match(protocols, /acceptance IDs/i);
|
|
876
312
|
assert.match(protocols, /current inline Receipt/i);
|
|
877
|
-
assert.match(
|
|
313
|
+
assert.match(regionsByName.get('sync').primary, /sync-finalize/i);
|
|
878
314
|
});
|
|
879
315
|
|
|
880
316
|
test('Develop process-first source contract preserves selection, recovery, final-Head, parallel, and Flash boundaries', () => {
|
|
@@ -1117,7 +553,7 @@ test('Claude installed Test preserves plan-native proof and references', () => {
|
|
|
1117
553
|
const installedReview = fs.readFileSync(path.join(root, '.claude/skills/code-review/SKILL.md'), 'utf8');
|
|
1118
554
|
assert.equal(installedReview, fs.readFileSync(CODE_REVIEW_SKILL, 'utf8'));
|
|
1119
555
|
assert.match(installedReview, /controller-validated `test-proof-v1`/);
|
|
1120
|
-
assert.match(installedReview, /
|
|
556
|
+
assert.match(installedReview, /For proof consumption, this skill's `## Execution-proof boundary` is\s+authoritative/i);
|
|
1121
557
|
assert.equal(fs.existsSync(path.join(root, '.agents/skills/test')), false);
|
|
1122
558
|
} finally {
|
|
1123
559
|
fs.rmSync(root, { recursive: true, force: true });
|
|
@@ -1131,432 +567,24 @@ test('specs-usage-guide documents Test proof handoff without timing claims', ()
|
|
|
1131
567
|
assert.match(section, /Test không ghi `Status:` hay inline[\s\S]*Develop controller là writer duy nhất/i);
|
|
1132
568
|
assert.match(section, /tracked\/untracked\/ignored drift/i);
|
|
1133
569
|
assert.match(section, /HTTPS\/localhost origin[\s\S]*fresh consent/i);
|
|
1134
|
-
assert.match(section, /
|
|
570
|
+
assert.match(section, /chỉ nằm trong `## Receipt` inline/i);
|
|
1135
571
|
assert.match(section, /không phải benchmark thời gian/i);
|
|
1136
572
|
assert.match(section, /không chứng minh live adherence/i);
|
|
1137
573
|
assert.doesNotMatch(section, /\b\d+(?:\.\d+)?\s*(?:ms|milliseconds?|seconds?|minutes?)\b/i);
|
|
1138
574
|
assert.doesNotMatch(section, /\[(?:LIVE_)?VERIFIED\]/i);
|
|
1139
575
|
});
|
|
1140
576
|
|
|
1141
|
-
test('canonical verdict adapter handles completion and unfinished decisions', () => {
|
|
1142
|
-
assert.deepEqual(POLICY.REVIEW_VERDICTS, ['PASS', 'PASS_WITH_WARNINGS', 'FAIL', 'BLOCKED']);
|
|
1143
|
-
assert.doesNotThrow(() => POLICY.assertVerdict('PASS_WITH_WARNINGS'));
|
|
1144
|
-
assert.throws(() => POLICY.assertVerdict('PARTIAL'), /Unsupported canonical verdict/);
|
|
1145
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS').completion, 'unfinished');
|
|
1146
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS').unfinished, true);
|
|
1147
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS').createsCompletion, false);
|
|
1148
|
-
assert.equal(POLICY.consumeReviewVerdict('PASS_WITH_WARNINGS').unfinished, true);
|
|
1149
|
-
assert.equal(POLICY.consumeReviewVerdict('FAIL').retry, true);
|
|
1150
|
-
assert.equal(POLICY.consumeReviewVerdict('BLOCKED').retry, false);
|
|
1151
|
-
assert.equal(POLICY.consumeReviewVerdict('BLOCKED').unfinished, true);
|
|
1152
|
-
assert.equal(POLICY.consumeReviewVerdict('PARTIAL').completion, 'unfinished');
|
|
1153
|
-
assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').completion, 'unfinished');
|
|
1154
|
-
assert.equal(POLICY.consumeReviewVerdict('PARTIAL').verdict, 'BLOCKED');
|
|
1155
|
-
assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').verdict, 'BLOCKED');
|
|
1156
|
-
assert.equal(POLICY.consumeReviewVerdict('PARTIAL').retry, false);
|
|
1157
|
-
assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').retry, false);
|
|
1158
|
-
assert.throws(() => POLICY.consumeReviewVerdict('UNKNOWN'), /Unsupported canonical verdict/);
|
|
1159
|
-
const cli = spawnSync(process.execPath, [POLICY_PATH, '--consume-verdict', '--verdict', 'BLOCKED', '--json'], { encoding: 'utf8' });
|
|
1160
|
-
assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
|
|
1161
|
-
assert.deepEqual(JSON.parse(cli.stdout).action, 'stop');
|
|
1162
|
-
assert.match(read(GATE), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
1163
|
-
assert.match(read(CODE_REVIEW_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
1164
|
-
assert.match(read(TEST_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
test('completion decision requires canonical execution receipt and every workflow obligation', () => {
|
|
1168
|
-
const receipt = canonicalReceipt();
|
|
1169
|
-
const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
|
|
1170
|
-
const audit = {
|
|
1171
|
-
schema_version: '1',
|
|
1172
|
-
reviewer_session_id: 'review-session-1',
|
|
1173
|
-
implementation_session_id: 'implementation-session-1',
|
|
1174
|
-
expected_provenance: { ...EXPECTED_PROVENANCE },
|
|
1175
|
-
evidence: 'independent audit report for the bound change',
|
|
1176
|
-
verdict: 'PASS',
|
|
1177
|
-
};
|
|
1178
|
-
const missing = POLICY.completionDecision('PASS', {
|
|
1179
|
-
workflow_policy: critical,
|
|
1180
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1181
|
-
execution_receipt: receipt,
|
|
1182
|
-
proofs: {
|
|
1183
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1184
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1185
|
-
},
|
|
1186
|
-
});
|
|
1187
|
-
assert.equal(missing.completion, 'unfinished');
|
|
1188
|
-
assert.ok(missing.missingProof.includes('needsIndependentAudit'));
|
|
1189
|
-
assert.equal(missing.unfinished, true);
|
|
1190
|
-
|
|
1191
|
-
const complete = POLICY.completionDecision('PASS', {
|
|
1192
|
-
workflow_policy: critical,
|
|
1193
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1194
|
-
execution_receipt: receipt,
|
|
1195
|
-
proofs: {
|
|
1196
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1197
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1198
|
-
needsIndependentAudit: audit,
|
|
1199
|
-
},
|
|
1200
|
-
});
|
|
1201
|
-
assert.equal(complete.completion, 'complete');
|
|
1202
|
-
assert.equal(complete.unfinished, false);
|
|
1203
|
-
assert.equal(complete.warnings, undefined);
|
|
1204
|
-
const warnings = POLICY.completionDecision('PASS_WITH_WARNINGS', {
|
|
1205
|
-
workflow_policy: critical,
|
|
1206
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1207
|
-
execution_receipt: receipt,
|
|
1208
|
-
proofs: {
|
|
1209
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1210
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1211
|
-
needsIndependentAudit: audit,
|
|
1212
|
-
},
|
|
1213
|
-
});
|
|
1214
|
-
assert.equal(warnings.completion, 'unfinished');
|
|
1215
|
-
assert.equal(warnings.unfinished, true);
|
|
1216
|
-
assert.match(warnings.blocker, /literal PASS/);
|
|
1217
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1218
|
-
workflow_policy: { proof_obligations: ['needsExecutionProof'] },
|
|
1219
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1220
|
-
execution_receipt: receipt,
|
|
1221
|
-
}).completion, 'unfinished');
|
|
1222
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1223
|
-
workflow_policy: {},
|
|
1224
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1225
|
-
execution_receipt: receipt,
|
|
1226
|
-
}).completion, 'unfinished');
|
|
1227
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1228
|
-
workflow_policy: critical,
|
|
1229
|
-
execution_receipt: receipt,
|
|
1230
|
-
}).completion, 'unfinished', 'arbitrary Base/Head without runtime binding must not complete');
|
|
1231
|
-
assert.equal(POLICY.completionDecision('PASS', {
|
|
1232
|
-
workflow_policy: critical,
|
|
1233
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1234
|
-
execution_receipt: receipt,
|
|
1235
|
-
proofs: {
|
|
1236
|
-
needsInspection: { evidence: 'inspection receipt' },
|
|
1237
|
-
needsResearchGrounding: { evidence: 'research receipt' },
|
|
1238
|
-
needsIndependentAudit: { independent: true, evidence: 'marker only', verdict: 'PASS' },
|
|
1239
|
-
},
|
|
1240
|
-
}).completion, 'unfinished', 'independent marker must not satisfy audit');
|
|
1241
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ independent: true }, EXPECTED_PROVENANCE).valid, false);
|
|
1242
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, verdict: 'PASS_WITH_WARNINGS' }, EXPECTED_PROVENANCE).valid, false);
|
|
1243
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, reviewer_session_id: audit.implementation_session_id }, EXPECTED_PROVENANCE).valid, false);
|
|
1244
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, expected_provenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }, EXPECTED_PROVENANCE).valid, false);
|
|
1245
|
-
assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, extra: 'forged' }, EXPECTED_PROVENANCE).valid, false);
|
|
1246
|
-
assert.equal(POLICY.validateIndependentAuditEvidence([audit], EXPECTED_PROVENANCE).valid, false);
|
|
1247
|
-
assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, RUNTIME_CONTEXT).valid, true);
|
|
1248
|
-
assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, { ...RUNTIME_CONTEXT }).valid, false, 'forged runtime session context must not satisfy audit binding');
|
|
1249
|
-
assert.equal(POLICY.completionDecision('PASS', {}).completion, 'unfinished');
|
|
1250
|
-
const artifactContext = {
|
|
1251
|
-
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
|
|
1252
|
-
receipt_binding: RECEIPT_BINDING,
|
|
1253
|
-
task_context: { artifacts: ['output/bundle.js'], expected_provenance: { ...EXPECTED_PROVENANCE } },
|
|
1254
|
-
execution_receipt: receipt,
|
|
1255
|
-
proofs: { needsInspection: { evidence: 'inspection receipt' } },
|
|
1256
|
-
receipt_options: { requireArtifactHash: false, artifactPaths: [] },
|
|
1257
|
-
};
|
|
1258
|
-
const artifactBlocked = POLICY.completionDecision('PASS', artifactContext);
|
|
1259
|
-
assert.equal(artifactBlocked.completion, 'unfinished', 'caller receipt options must not weaken task artifact requirements');
|
|
1260
|
-
assert.ok(artifactBlocked.missingProof.some((item) => item.includes('artifact_hash')));
|
|
1261
|
-
});
|
|
1262
|
-
|
|
1263
|
-
test('auto authoring reaches readiness only after promotion, validation, grounding, and current review', () => {
|
|
1264
|
-
const fixture = createCanonicalAuthoringFixture();
|
|
1265
|
-
try {
|
|
1266
|
-
let spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1267
|
-
assert.equal(spec.semantic_model, null);
|
|
1268
|
-
spec.ready_for_implementation = true;
|
|
1269
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1270
|
-
let validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
1271
|
-
assert.ok(validation.errors.some((error) => error.includes('requires explicit promoted machine semantic authority')));
|
|
1272
|
-
|
|
1273
|
-
spec.ready_for_implementation = false;
|
|
1274
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1275
|
-
promoteCanonicalModel(fixture);
|
|
1276
|
-
spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1277
|
-
assert.ok(spec.semantic_model && spec.semantic_model.criteria.length === 2);
|
|
1278
|
-
spec.ready_for_implementation = true;
|
|
1279
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1280
|
-
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
1281
|
-
assert.ok(validation.errors.some((error) => error.includes('readiness requires validated')));
|
|
1282
|
-
assert.ok(validation.errors.some((error) => error.includes('requires completed semantic review')));
|
|
1283
|
-
|
|
1284
|
-
spec.ready_for_implementation = false;
|
|
1285
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1286
|
-
spec = makeCanonicalReady(fixture);
|
|
1287
|
-
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
1288
|
-
assert.deepEqual(validation.errors, [], validation.errors.join('\n'));
|
|
1289
|
-
assert.equal(GROUNDER.groundSpec({ specDir: fixture.specDir, root: fixture.root, spec }).errors.length, 0);
|
|
1290
|
-
|
|
1291
|
-
const specs = read(SPECS);
|
|
1292
|
-
const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
|
|
1293
|
-
assert.ok(executableBlocks.every((block) => !/cf:?develop/i.test(block)), 'Specs must never execute Develop');
|
|
1294
|
-
assert.match(specs, /An implementation workflow owns execution/i);
|
|
1295
|
-
assert.match(specs, /selects one unblocked task[\s\S]*runs\s+the task's verification[\s\S]*inline `## Receipt`/i);
|
|
1296
|
-
assert.match(specs, /The user decides\s+at C3 whether the feature is done/i);
|
|
1297
|
-
} finally {
|
|
1298
|
-
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
1299
|
-
}
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
test('atomic readiness supports one-criterion Routine specs and binds semantic text', () => {
|
|
1303
|
-
const fixture = createOrdinaryAuthoringFixture();
|
|
1304
|
-
try {
|
|
1305
|
-
const authored = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1306
|
-
authored.decisions = [
|
|
1307
|
-
{ id: 'Q1', classification: 'repository_fact', statement: 'The runtime entrypoint is src/entry.js.', status: 'grounded', evidence: 'Repository anchor A-D-01 resolves to src/entry.js.' },
|
|
1308
|
-
{ id: 'Q2', classification: 'reversible_assumption', statement: 'Use the existing direct module export for this bounded change.', status: 'recorded', evidence: 'Reversal is limited to src/entry.js and does not alter product data.' },
|
|
1309
|
-
];
|
|
1310
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(authored, null, 2)}\n`);
|
|
1311
|
-
const result = READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() });
|
|
1312
|
-
assert.equal(result.spec.ready_for_implementation, true);
|
|
1313
|
-
assert.equal(result.spec.semantic_model.criteria.length, 1);
|
|
1314
|
-
assert.match(result.spec.semantic_model.criteria[0].text, /delete the selected record/);
|
|
1315
|
-
assert.deepEqual(VALIDATOR.validateSpec(fixture.specDir).errors, []);
|
|
1316
|
-
|
|
1317
|
-
const beforeDigest = result.semantic_digest;
|
|
1318
|
-
const design = path.join(fixture.specDir, 'design.md');
|
|
1319
|
-
const originalDesign = fs.readFileSync(design, 'utf8');
|
|
1320
|
-
fs.writeFileSync(design, originalDesign.replace('deleted rather than archived', 'archived rather than deleted'));
|
|
1321
|
-
const decisionProjection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
1322
|
-
assert.notDeepEqual(decisionProjection.model.design_records, result.spec.semantic_model.design_records);
|
|
1323
|
-
const decisionDigest = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: decisionProjection.model });
|
|
1324
|
-
assert.equal(decisionDigest.errors.length, 0, decisionDigest.errors.join('\n'));
|
|
1325
|
-
assert.notEqual(decisionDigest.digest, beforeDigest);
|
|
1326
|
-
fs.writeFileSync(design, originalDesign);
|
|
1327
|
-
|
|
1328
|
-
const requirements = path.join(fixture.specDir, 'requirements.md');
|
|
1329
|
-
fs.writeFileSync(requirements, fs.readFileSync(requirements, 'utf8').replace('delete the selected record', 'archive the selected record'));
|
|
1330
|
-
const projection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
|
|
1331
|
-
assert.notEqual(projection.model.criteria[0].text, result.spec.semantic_model.criteria[0].text);
|
|
1332
|
-
const changed = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: projection.model });
|
|
1333
|
-
assert.equal(changed.errors.length, 0, changed.errors.join('\n'));
|
|
1334
|
-
assert.notEqual(changed.digest, beforeDigest);
|
|
1335
|
-
assert.ok(VALIDATOR.validateSpec(fixture.specDir).errors.some((error) => /projection|stale/.test(error)));
|
|
1336
|
-
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
test('atomic readiness preserves exact bytes on review, grounding, decision, and Strict authority failures', () => {
|
|
1340
|
-
const fixture = createOrdinaryAuthoringFixture();
|
|
1341
|
-
try {
|
|
1342
|
-
const original = fs.readFileSync(fixture.specPath);
|
|
1343
|
-
for (const mutate of [
|
|
1344
|
-
(review) => ({ ...review, reviewed_criteria: [] }),
|
|
1345
|
-
(review) => ({ ...review, counterexamples: [] }),
|
|
1346
|
-
]) {
|
|
1347
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: mutate(ordinaryReview()) }), /readiness validation/);
|
|
1348
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
1349
|
-
}
|
|
1350
|
-
fs.unlinkSync(path.join(fixture.root, 'src', 'entry.js'));
|
|
1351
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /grounding|validation/);
|
|
1352
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
1353
|
-
fs.writeFileSync(path.join(fixture.root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
|
|
1354
|
-
|
|
1355
|
-
process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME = '1';
|
|
1356
|
-
try {
|
|
1357
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /injected finalization failure/);
|
|
1358
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
|
|
1359
|
-
} finally { delete process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME; }
|
|
1360
|
-
|
|
1361
|
-
const blocked = JSON.parse(original);
|
|
1362
|
-
blocked.decisions = [{ id: 'Q1', classification: 'user_owned', statement: 'Choose permanent deletion or retention archive behavior.', status: 'unresolved', evidence: null }];
|
|
1363
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
1364
|
-
const blockedBytes = fs.readFileSync(fixture.specPath);
|
|
1365
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /unresolved material user-owned decision/);
|
|
1366
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(blockedBytes), true);
|
|
1367
|
-
|
|
1368
|
-
delete blocked.decisions;
|
|
1369
|
-
blocked.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
1370
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
1371
|
-
const strictBytes = fs.readFileSync(fixture.specPath);
|
|
1372
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
|
|
1373
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(strictBytes), true);
|
|
1374
|
-
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
test('Strict atomic readiness succeeds only after current allowlisted host observation', () => {
|
|
1378
|
-
const fixture = createOrdinaryAuthoringFixture();
|
|
1379
|
-
try {
|
|
1380
|
-
const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1381
|
-
spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
1382
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1383
|
-
const before = fs.readFileSync(fixture.specPath);
|
|
1384
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
|
|
1385
|
-
assert.equal(fs.readFileSync(fixture.specPath).equals(before), true);
|
|
1386
|
-
|
|
1387
|
-
const digest = VALIDATOR.computeSemanticDigest21(fixture.specDir, spec);
|
|
1388
|
-
assert.equal(digest.errors.length, 0, digest.errors.join('\n'));
|
|
1389
|
-
const authorityHome = path.join(fixture.root, 'authority-home');
|
|
1390
|
-
const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({ feature_name: 'ordinary', spec_file: 'specs/ordinary/spec.json', semantic_digest: digest.digest, verdict: 'PASS' })}`;
|
|
1391
|
-
const observed = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
|
|
1392
|
-
cwd: fixture.root,
|
|
1393
|
-
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
1394
|
-
input: JSON.stringify({ hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'reviewer-1', agent_type: 'code_auditor', last_assistant_message: claim, cwd: fixture.root }),
|
|
1395
|
-
encoding: 'utf8',
|
|
1396
|
-
});
|
|
1397
|
-
assert.equal(observed.status, 0, observed.stderr);
|
|
1398
|
-
const reviewFile = path.join(fixture.root, 'review.json');
|
|
1399
|
-
fs.writeFileSync(reviewFile, `${JSON.stringify(ordinaryReview())}\n`);
|
|
1400
|
-
const finalized = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'), fixture.specDir, '--review-result', reviewFile], {
|
|
1401
|
-
cwd: fixture.root,
|
|
1402
|
-
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
1403
|
-
encoding: 'utf8',
|
|
1404
|
-
});
|
|
1405
|
-
assert.equal(finalized.status, 0, `${finalized.stdout}\n${finalized.stderr}`);
|
|
1406
|
-
assert.equal(JSON.parse(fs.readFileSync(fixture.specPath, 'utf8')).ready_for_implementation, true);
|
|
1407
|
-
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
test('done is a final-state request bound to current semantic model and digest', () => {
|
|
1411
|
-
const fixture = createCanonicalAuthoringFixture();
|
|
1412
|
-
try {
|
|
1413
|
-
promoteCanonicalModel(fixture);
|
|
1414
|
-
const spec = makeCanonicalReady(fixture);
|
|
1415
|
-
spec.status = 'done';
|
|
1416
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1417
|
-
const candidate = {
|
|
1418
|
-
featureDir: fixture.specDir,
|
|
1419
|
-
featureName: 'feature',
|
|
1420
|
-
specFile: fixture.specPath,
|
|
1421
|
-
spec,
|
|
1422
|
-
};
|
|
1423
|
-
const dependencies = () => ({
|
|
1424
|
-
validator: VALIDATOR,
|
|
1425
|
-
grounder: GROUNDER,
|
|
1426
|
-
semanticAuthority: SEMANTIC_AUTHORITY,
|
|
1427
|
-
});
|
|
1428
|
-
const current = FINAL_STATE.validateCanonicalFinalState({
|
|
1429
|
-
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1430
|
-
});
|
|
1431
|
-
assert.equal(current.ok, true, current.reason);
|
|
1432
|
-
assert.equal(current.semanticDigest, spec.validation.semantic_review.semantic_digest);
|
|
1433
|
-
|
|
1434
|
-
const failedReceipt = structuredClone(spec);
|
|
1435
|
-
failedReceipt.validation.semantic_review.verdict = 'FAIL';
|
|
1436
|
-
const receiptMutation = FINAL_STATE.validateCanonicalFinalState({
|
|
1437
|
-
policy: POLICY,
|
|
1438
|
-
projectRoot: fixture.root,
|
|
1439
|
-
candidate: { ...candidate, spec: failedReceipt },
|
|
1440
|
-
dependencies,
|
|
1441
|
-
});
|
|
1442
|
-
assert.equal(receiptMutation.ok, false);
|
|
1443
|
-
assert.match(receiptMutation.reason, /PASS CONTINUE/);
|
|
1444
|
-
|
|
1445
|
-
const mismatchedHistory = structuredClone(spec);
|
|
1446
|
-
const latest = mismatchedHistory.validation.semantic_review_history.entries[
|
|
1447
|
-
mismatchedHistory.validation.semantic_review_history.entries.length - 1
|
|
1448
|
-
];
|
|
1449
|
-
latest.review_receipt_digest = `sha256:${'0'.repeat(64)}`;
|
|
1450
|
-
const digestMismatch = FINAL_STATE.validateCanonicalFinalState({
|
|
1451
|
-
policy: POLICY,
|
|
1452
|
-
projectRoot: fixture.root,
|
|
1453
|
-
candidate: { ...candidate, spec: mismatchedHistory },
|
|
1454
|
-
dependencies,
|
|
1455
|
-
});
|
|
1456
|
-
assert.equal(digestMismatch.ok, false);
|
|
1457
|
-
assert.match(digestMismatch.reason, /review_receipt_digest|canonical receipt/);
|
|
1458
|
-
|
|
1459
|
-
const requirementsPath = path.join(fixture.specDir, 'requirements.md');
|
|
1460
|
-
const requirements = fs.readFileSync(requirementsPath, 'utf8');
|
|
1461
|
-
fs.writeFileSync(requirementsPath, requirements.replace('invalid input shall return rejected state', 'invalid input shall return a deterministic rejected state'));
|
|
1462
|
-
const staleDigest = FINAL_STATE.validateCanonicalFinalState({
|
|
1463
|
-
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
1464
|
-
});
|
|
1465
|
-
assert.equal(staleDigest.ok, false);
|
|
1466
|
-
assert.match(staleDigest.reason, /stale|differs from Markdown projection/i);
|
|
1467
|
-
|
|
1468
|
-
fs.writeFileSync(requirementsPath, requirements);
|
|
1469
|
-
const staleModel = structuredClone(spec);
|
|
1470
|
-
staleModel.semantic_model.criteria[0].owner = 'A-D-02';
|
|
1471
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(staleModel, null, 2)}\n`);
|
|
1472
|
-
const invalidModel = FINAL_STATE.validateCanonicalFinalState({
|
|
1473
|
-
policy: POLICY,
|
|
1474
|
-
projectRoot: fixture.root,
|
|
1475
|
-
candidate: { ...candidate, spec: staleModel },
|
|
1476
|
-
dependencies,
|
|
1477
|
-
});
|
|
1478
|
-
assert.equal(invalidModel.ok, false);
|
|
1479
|
-
assert.match(invalidModel.reason, /semantic projection|semantic_model|validation failed/i);
|
|
1480
|
-
} finally {
|
|
1481
|
-
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
1482
|
-
}
|
|
1483
|
-
});
|
|
1484
|
-
|
|
1485
|
-
test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
|
|
1486
|
-
const initial = canonicalFlashTask();
|
|
1487
|
-
for (const verdict of ['FAIL', 'BLOCKED', 'PARTIAL', 'NO_TESTS', 'PASS_WITH_WARNINGS']) {
|
|
1488
|
-
const result = POLICY.promoteFlashTask(initial, verdict);
|
|
1489
|
-
assert.equal(result.status, 'in_progress');
|
|
1490
|
-
assert.equal(result.receipt, 'FLASH_UNVERIFIED');
|
|
1491
|
-
assert.equal(result.dependencyBlocked, true);
|
|
1492
|
-
assert.equal(result.unblocks, false);
|
|
1493
|
-
assert.match(result.blocker, /verification|test proof/);
|
|
1494
|
-
}
|
|
1495
|
-
const promoted = POLICY.promoteFlashTask(initial, 'PASS', canonicalReceipt());
|
|
1496
|
-
assert.equal(promoted.status, 'in_progress');
|
|
1497
|
-
assert.equal(promoted.receipt.startsWith('Verification: PASS'), true);
|
|
1498
|
-
assert.equal(promoted.blocker, null);
|
|
1499
|
-
assert.equal(promoted.dependencyBlocked, true);
|
|
1500
|
-
assert.equal(promoted.unblocks, false);
|
|
1501
|
-
assert.equal(promoted.readyForSync, true);
|
|
1502
|
-
assert.equal(POLICY.finalizeFlashTask(promoted, 'sync'), promoted);
|
|
1503
|
-
const forgedFinalize = POLICY.finalizeFlashTask(promoted, 'sync-finalize');
|
|
1504
|
-
assert.equal(forgedFinalize.status, 'in_progress');
|
|
1505
|
-
assert.equal(forgedFinalize.dependencyBlocked, true);
|
|
1506
|
-
assert.equal(forgedFinalize.unblocks, false);
|
|
1507
|
-
const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', promoted.receipt);
|
|
1508
|
-
assert.equal(finalized.status, 'done');
|
|
1509
|
-
assert.equal(finalized.dependencyBlocked, false);
|
|
1510
|
-
assert.equal(finalized.unblocks, true);
|
|
1511
|
-
assert.equal(finalized.readyForSync, false);
|
|
1512
|
-
assert.equal(POLICY.isStaleFlashDone({ status: 'done', receipt: 'FLASH_UNVERIFIED' }), true);
|
|
1513
|
-
assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/i);
|
|
1514
|
-
assert.match(read(SYNC_SKILL), /sync-finalize/);
|
|
1515
|
-
});
|
|
1516
|
-
|
|
1517
|
-
test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
|
|
1518
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-spec-gate-'));
|
|
1519
|
-
const specGate = installClaudeRuntimeClosure(root);
|
|
1520
|
-
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'spec-final-state.cjs')), true);
|
|
1521
|
-
assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'validate-spec-output.cjs')), true);
|
|
1522
|
-
fs.mkdirSync(path.join(root, 'specs', 'demo', 'tasks'), { recursive: true });
|
|
1523
|
-
fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
|
|
1524
|
-
status: 'in_progress',
|
|
1525
|
-
current_phase: 'closeout',
|
|
1526
|
-
feature_name: 'demo',
|
|
1527
|
-
task_registry: { 'tasks/task.md': { status: 'done', receipt: 'FLASH_UNVERIFIED' } },
|
|
1528
|
-
}));
|
|
1529
|
-
initFixtureGit(root);
|
|
1530
|
-
try {
|
|
1531
|
-
const result = spawnSync(process.execPath, [specGate], {
|
|
1532
|
-
cwd: root,
|
|
1533
|
-
env: { ...process.env, PROJECT_ROOT: root },
|
|
1534
|
-
input: JSON.stringify({ cwd: root, session_id: 'test' }),
|
|
1535
|
-
encoding: 'utf8',
|
|
1536
|
-
});
|
|
1537
|
-
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
1538
|
-
const payload = JSON.parse(result.stdout);
|
|
1539
|
-
assert.equal(payload.decision, 'block');
|
|
1540
|
-
assert.match(payload.reason, /FLASH_UNVERIFIED/);
|
|
1541
|
-
} finally {
|
|
1542
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
1543
|
-
}
|
|
1544
|
-
});
|
|
1545
|
-
|
|
1546
577
|
test('first-run cache bypass is blocked - canonical receipt required even without cache', () => {
|
|
1547
578
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-first-run-'));
|
|
1548
579
|
const claude = path.join(root, '.claude');
|
|
1549
580
|
const specGate = installClaudeRuntimeClosure(root);
|
|
1550
|
-
const
|
|
1551
|
-
fs.mkdirSync(
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
task_registry: { 'tasks/task-R1-01-one.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
|
|
1558
|
-
}));
|
|
1559
|
-
fs.writeFileSync(path.join(specDir, 'task-R1-01-one.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\n```\nnpm test\n```\n');
|
|
581
|
+
const featureDir = path.join(root, 'specs', 'demo');
|
|
582
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
583
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo\nSpecs-Contract: process-first-ready-v1\n');
|
|
584
|
+
const taskFile = path.join(featureDir, 'task-01-one.md');
|
|
585
|
+
const head = '# Task 01: One\n\nStatus: done\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- Command: pnpm test\n\n';
|
|
586
|
+
// Done task without a canonical receipt (no Command, no provenance)
|
|
587
|
+
fs.writeFileSync(taskFile, `${head}## Receipt\n\nVerification: PASS\n\`\`\`\nnpm test\n\`\`\`\n`);
|
|
1560
588
|
initFixtureGit(root);
|
|
1561
589
|
try {
|
|
1562
590
|
assert.equal(fs.existsSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')), false);
|
|
@@ -1569,22 +597,18 @@ test('first-run cache bypass is blocked - canonical receipt required even withou
|
|
|
1569
597
|
assert.equal(result.status, 0);
|
|
1570
598
|
const payload = JSON.parse(result.stdout);
|
|
1571
599
|
assert.equal(payload.decision, 'block', 'first-run without canonical receipt must block');
|
|
1572
|
-
assert.match(payload.reason, /
|
|
600
|
+
assert.match(payload.reason, /task-01-one\.md/);
|
|
1573
601
|
assert.match(payload.reason, /receipt|command|provenance/i);
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
);
|
|
1581
|
-
fs.writeFileSync(
|
|
1582
|
-
path.join(root, 'specs', 'demo', 'feature-receipt.md'),
|
|
1583
|
-
bindFixtureReceipt(root, '# Feature Receipt\n\nFeature: demo\nVerification: PASS\nCommand: pnpm test\nExit: 0\nResult: PASS\nExpected: integrated feature passes\nObserved: integrated feature passed\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n'),
|
|
1584
|
-
);
|
|
602
|
+
const context = PROVENANCE_HELPER.deriveRuntimeContext({
|
|
603
|
+
projectRoot: root,
|
|
604
|
+
specsRoot: path.join(root, 'specs'),
|
|
605
|
+
specFile: path.join(featureDir, 'plan.md'),
|
|
606
|
+
featureName: 'demo',
|
|
607
|
+
runtimeSession: 'test',
|
|
608
|
+
});
|
|
609
|
+
fs.writeFileSync(taskFile, `${head}## Receipt\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: ${context.base}\nHead: ${context.head}\n\`\`\`\npnpm test\nPASS\n\`\`\`\n`);
|
|
1585
610
|
// Clear cache to simulate fresh first-run with valid receipt
|
|
1586
611
|
try { fs.unlinkSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')); } catch {}
|
|
1587
|
-
// Re-run with valid receipt - should not block
|
|
1588
612
|
const result2 = spawnSync(process.execPath, [specGate], {
|
|
1589
613
|
cwd: root,
|
|
1590
614
|
env: { ...process.env, PROJECT_ROOT: root },
|
|
@@ -1607,10 +631,6 @@ test('canonical receipt requires command, exit, provenance and unambiguous PASS'
|
|
|
1607
631
|
assert.ok(missingVerification.includes('verification_state'));
|
|
1608
632
|
const artifactWithoutHash = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: dist/bundle.js\n');
|
|
1609
633
|
assert.ok(artifactWithoutHash.includes('artifact_hash'));
|
|
1610
|
-
// CLI validation
|
|
1611
|
-
const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n' }), '--json'], { encoding: 'utf8' });
|
|
1612
|
-
assert.equal(cli.status, 0);
|
|
1613
|
-
assert.equal(JSON.parse(cli.stdout).ok, true);
|
|
1614
634
|
});
|
|
1615
635
|
|
|
1616
636
|
test('canonical receipt provenance requires both Base and Head (or both base_sha and head_sha)', () => {
|
|
@@ -1660,27 +680,10 @@ test('canonical receipt provenance requires both Base and Head (or both base_sha
|
|
|
1660
680
|
assert.deepEqual(validWithValues, [], 'valid Base and Head with values should pass');
|
|
1661
681
|
const validShaWithValues = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: 0123456789abcdef0123456789abcdef01234567\nhead_sha: 89abcdef0123456789abcdef0123456789abcdef\n');
|
|
1662
682
|
assert.deepEqual(validShaWithValues, [], 'valid base_sha and head_sha with values should pass');
|
|
1663
|
-
// CLI: only Base via --validate-receipt should fail
|
|
1664
|
-
const cliOnlyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\n' }), '--json'], { encoding: 'utf8' });
|
|
1665
|
-
assert.equal(cliOnlyBase.status, 2);
|
|
1666
|
-
assert.equal(JSON.parse(cliOnlyBase.stdout).ok, false);
|
|
1667
|
-
assert.ok(JSON.parse(cliOnlyBase.stdout).failures.includes('provenance'));
|
|
1668
|
-
const cliBoth = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n' }), '--json'], { encoding: 'utf8' });
|
|
1669
|
-
assert.equal(cliBoth.status, 0);
|
|
1670
|
-
assert.equal(JSON.parse(cliBoth.stdout).ok, true);
|
|
1671
|
-
// CLI empty Base should fail
|
|
1672
|
-
const cliEmptyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: b\n' }), '--json'], { encoding: 'utf8' });
|
|
1673
|
-
assert.equal(cliEmptyBase.status, 2);
|
|
1674
|
-
assert.ok(JSON.parse(cliEmptyBase.stdout).failures.includes('provenance'));
|
|
1675
683
|
});
|
|
1676
684
|
|
|
1677
685
|
test('canonical receipt binds expected provenance when the runtime supplies it', () => {
|
|
1678
686
|
const body = canonicalReceipt();
|
|
1679
|
-
const binding = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
|
|
1680
|
-
assert.deepEqual(binding.expectedProvenance, EXPECTED_PROVENANCE);
|
|
1681
|
-
assert.equal(binding.requireProvenanceBinding, true);
|
|
1682
|
-
assert.equal(binding.runtimeContext.context_id, RUNTIME_CONTEXT.context_id);
|
|
1683
|
-
assert.throws(() => POLICY.createReceiptBinding(EXPECTED_PROVENANCE), /runtime-derived provenance context/);
|
|
1684
687
|
assert.ok(POLICY.validateCanonicalReceipt(body, { requireProvenanceBinding: true }).includes('provenance'));
|
|
1685
688
|
assert.deepEqual(POLICY.validateCanonicalReceipt(body, { expectedProvenance: EXPECTED_PROVENANCE }), []);
|
|
1686
689
|
assert.ok(POLICY.validateCanonicalReceipt(body, { expectedProvenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }).includes('provenance'));
|
|
@@ -1774,37 +777,24 @@ test('runtime provenance derives exact Git evidence, CLI context, and stale/forg
|
|
|
1774
777
|
assert.equal(withIgnoredCache.head, initial.head, 'nested ignored node_modules/cache files must not enter Head');
|
|
1775
778
|
assert.equal(withChangedIgnoredCache.head, initial.head, 'changing nested ignored cache data must not change Head');
|
|
1776
779
|
|
|
1777
|
-
|
|
1778
|
-
const forgedDecision = POLICY.completionDecision('PASS', {
|
|
1779
|
-
workflow_policy: { proof_obligations: ['needsExecutionProof'] },
|
|
1780
|
-
runtime_context: forgedContext,
|
|
1781
|
-
execution_receipt: exactReceipt,
|
|
1782
|
-
});
|
|
1783
|
-
assert.equal(forgedDecision.completion, 'unfinished');
|
|
1784
|
-
assert.deepEqual(forgedDecision.missingProof, ['runtime_provenance']);
|
|
1785
|
-
|
|
1786
|
-
const flash = {
|
|
1787
|
-
status: 'in_progress',
|
|
1788
|
-
receipt: 'FLASH_UNVERIFIED',
|
|
1789
|
-
blocker: 'awaiting test proof',
|
|
1790
|
-
dependencyBlocked: true,
|
|
1791
|
-
unblocks: false,
|
|
1792
|
-
runtime_context: initial,
|
|
1793
|
-
};
|
|
1794
|
-
const promoted = POLICY.promoteFlashTask(flash, 'PASS', exactReceipt);
|
|
1795
|
-
assert.equal(promoted.readyForSync, true);
|
|
1796
|
-
|
|
780
|
+
// A source change after the receipt was written leaves its Base/Head stale.
|
|
1797
781
|
fs.writeFileSync(sourceFile, 'mutated source\n');
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
assert.
|
|
782
|
+
// The pre-change context is recomputed against the current tree, so the old pair no longer binds.
|
|
783
|
+
assert.ok(
|
|
784
|
+
POLICY.validateCanonicalReceipt(exactReceipt, POLICY.receiptValidatorOptions({}, {
|
|
785
|
+
runtimeContext: initial,
|
|
786
|
+
requireProvenanceBinding: true,
|
|
787
|
+
})).includes('provenance'),
|
|
788
|
+
'a receipt bound to the pre-change tree must go stale',
|
|
789
|
+
);
|
|
790
|
+
// A copied context is not runtime-derived and must never supply the expected pair.
|
|
791
|
+
assert.ok(
|
|
792
|
+
POLICY.validateCanonicalReceipt(exactReceipt, POLICY.receiptValidatorOptions({}, {
|
|
793
|
+
runtimeContext: { ...initial },
|
|
794
|
+
requireProvenanceBinding: true,
|
|
795
|
+
})).includes('provenance'),
|
|
796
|
+
'a copied runtime context must not bind',
|
|
797
|
+
);
|
|
1808
798
|
|
|
1809
799
|
fs.writeFileSync(path.join(root, 'src', 'untracked.js'), 'untracked source\n');
|
|
1810
800
|
const withUntracked = PROVENANCE_HELPER.deriveRuntimeContext(input);
|
|
@@ -1898,76 +888,6 @@ test('runtime provenance chooses a deterministic root when all history is Specs-
|
|
|
1898
888
|
}
|
|
1899
889
|
});
|
|
1900
890
|
|
|
1901
|
-
test('lane traces - Direct, Standard, explicit Strict classification, override, state mutation and completion', () => {
|
|
1902
|
-
// Direct: isolated reversible low-risk
|
|
1903
|
-
const direct = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
|
|
1904
|
-
assert.equal(direct.lane, 'Direct');
|
|
1905
|
-
assert.equal(direct.automaticLane, 'Direct');
|
|
1906
|
-
assert.deepEqual(POLICY.lanePolicy(direct).proof_obligations, ['needsExecutionProof']);
|
|
1907
|
-
assert.equal(POLICY.lanePolicy(direct).requiresSpec, false);
|
|
1908
|
-
assert.equal(POLICY.lanePolicy(direct).shipPoint, 'task');
|
|
1909
|
-
// Standard: default
|
|
1910
|
-
const standard = POLICY.classifyLane({ title: 'add pagination to list', taskCount: 1 });
|
|
1911
|
-
assert.equal(standard.lane, 'Standard');
|
|
1912
|
-
assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['needsExecutionProof']);
|
|
1913
|
-
assert.equal(POLICY.lanePolicy(standard).shipPoint, 'feature');
|
|
1914
|
-
// Risk alone is Elevated; Critical requires an explicit Strict assurance choice.
|
|
1915
|
-
const elevated = POLICY.classifyLane({ riskSignals: { auth: true }, taskCount: 1 });
|
|
1916
|
-
assert.equal(elevated.lane, 'Standard');
|
|
1917
|
-
assert.deepEqual(POLICY.lanePolicy(elevated).proof_obligations, ['needsInspection', 'needsExecutionProof']);
|
|
1918
|
-
const critical = POLICY.classifyLane({ riskSignals: { auth: true }, assurance_level: 'Strict', taskCount: 1 });
|
|
1919
|
-
assert.equal(critical.lane, 'Critical');
|
|
1920
|
-
assert.ok(critical.risks.includes('auth'));
|
|
1921
|
-
assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
|
|
1922
|
-
// Override: Direct requesting Critical is allowed (upgrade) without extra auth
|
|
1923
|
-
const upgrade = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1, override: 'Critical' });
|
|
1924
|
-
assert.equal(upgrade.lane, 'Critical');
|
|
1925
|
-
assert.equal(upgrade.automaticLane, 'Direct');
|
|
1926
|
-
// Downgrade without auth is blocked
|
|
1927
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /assurance_level downgrade.*blocked/i);
|
|
1928
|
-
// Caller-owned booleans cannot weaken the classified minimum.
|
|
1929
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true }), /assurance_level downgrade.*blocked/i);
|
|
1930
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', user_approved: true }), /assurance_level downgrade.*blocked/i);
|
|
1931
|
-
// State mutation: approvalState
|
|
1932
|
-
const pendingApproval = POLICY.approvalState({ generated: true, agent_validated: false, user_approved: false });
|
|
1933
|
-
assert.equal(pendingApproval.ready, false);
|
|
1934
|
-
const agentValidated = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false });
|
|
1935
|
-
assert.equal(agentValidated.ready, true, 'generated plus agent validation is technical readiness');
|
|
1936
|
-
const approved = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true });
|
|
1937
|
-
assert.equal(approved.ready, true);
|
|
1938
|
-
// Completion: flash work remains in_progress and does not unblock
|
|
1939
|
-
const flashTask = canonicalFlashTask();
|
|
1940
|
-
const failRemains = POLICY.promoteFlashTask(flashTask, 'FAIL');
|
|
1941
|
-
assert.equal(failRemains.status, 'in_progress');
|
|
1942
|
-
assert.equal(failRemains.unblocks, false);
|
|
1943
|
-
const blockedRemains = POLICY.promoteFlashTask(flashTask, 'BLOCKED');
|
|
1944
|
-
assert.equal(blockedRemains.unblocks, false);
|
|
1945
|
-
const noTestsRemains = POLICY.promoteFlashTask(flashTask, 'NO_TESTS');
|
|
1946
|
-
assert.equal(noTestsRemains.unblocks, false);
|
|
1947
|
-
const promoted = POLICY.promoteFlashTask(flashTask, 'PASS', canonicalReceipt());
|
|
1948
|
-
assert.equal(promoted.readyForSync, true);
|
|
1949
|
-
assert.equal(promoted.unblocks, false, 'promoted flash must not unblock until sync-finalize');
|
|
1950
|
-
const finalized = POLICY.syncFinalizeFlashTask(flashTask, 'PASS', promoted.receipt);
|
|
1951
|
-
assert.equal(finalized.status, 'done');
|
|
1952
|
-
assert.equal(finalized.unblocks, true);
|
|
1953
|
-
});
|
|
1954
|
-
|
|
1955
|
-
test('flash selective promotion - only specific task is promoted, not blanket', () => {
|
|
1956
|
-
const registry = {
|
|
1957
|
-
'tasks/task-a.md': canonicalFlashTask(),
|
|
1958
|
-
'tasks/task-b.md': canonicalFlashTask(),
|
|
1959
|
-
};
|
|
1960
|
-
const promotedA = POLICY.promoteFlashTask(registry['tasks/task-a.md'], 'PASS', canonicalReceipt());
|
|
1961
|
-
const notPromotedB = registry['tasks/task-b.md']; // unchanged
|
|
1962
|
-
assert.equal(promotedA.readyForSync, true);
|
|
1963
|
-
assert.equal(notPromotedB.receipt, 'FLASH_UNVERIFIED');
|
|
1964
|
-
assert.equal(notPromotedB.readyForSync, undefined);
|
|
1965
|
-
// FAIL on B does not affect A
|
|
1966
|
-
const failB = POLICY.promoteFlashTask(notPromotedB, 'FAIL');
|
|
1967
|
-
assert.equal(failB.receipt, 'FLASH_UNVERIFIED');
|
|
1968
|
-
assert.equal(promotedA.receipt.startsWith('Verification: PASS'), true);
|
|
1969
|
-
});
|
|
1970
|
-
|
|
1971
891
|
test('parallel waves require immutable provenance receipts and safe recovery', () => {
|
|
1972
892
|
const waves = markdownLegacyRegions(read(PARALLEL_WAVES)).primary;
|
|
1973
893
|
assert.match(waves, /only for explicit parallel execution/i);
|
|
@@ -2675,36 +1595,6 @@ test('P0 receipt fail-closed: Exit 1/-1/abc/conflict/empty command rejected', ()
|
|
|
2675
1595
|
assert.deepEqual(fails, [], `case ${desc} should pass, got ${fails}`);
|
|
2676
1596
|
}
|
|
2677
1597
|
}
|
|
2678
|
-
// CLI parity: Exit 1 must be rejected via CLI
|
|
2679
|
-
const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: `${base}Exit: 1\n` }), '--json'], { encoding: 'utf8' });
|
|
2680
|
-
assert.equal(cli.status, 2);
|
|
2681
|
-
assert.equal(JSON.parse(cli.stdout).ok, false);
|
|
2682
|
-
});
|
|
2683
|
-
|
|
2684
|
-
test('P0 flash marker-only must not promote or finalize', () => {
|
|
2685
|
-
const flash = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
|
|
2686
|
-
const markerOnly = 'Verification: PASS';
|
|
2687
|
-
const promoted = POLICY.promoteFlashTask(flash, 'PASS', markerOnly);
|
|
2688
|
-
assert.equal(promoted.status, 'in_progress');
|
|
2689
|
-
assert.equal(promoted.receipt, 'FLASH_UNVERIFIED');
|
|
2690
|
-
assert.equal(promoted.readyForSync, false);
|
|
2691
|
-
assert.equal(promoted.dependencyBlocked, true);
|
|
2692
|
-
assert.equal(promoted.unblocks, false);
|
|
2693
|
-
assert.match(promoted.blocker, /canonical receipt/);
|
|
2694
|
-
// Even with PASS marker but missing command/exit/provenance, finalize must not complete
|
|
2695
|
-
const fakePromoted = { status: 'in_progress', receipt: markerOnly, readyForSync: true, dependencyBlocked: true, unblocks: false };
|
|
2696
|
-
const notFinalized = POLICY.finalizeFlashTask(fakePromoted, 'sync-finalize');
|
|
2697
|
-
assert.equal(notFinalized.status, 'in_progress', 'marker-only receipt must not finalize');
|
|
2698
|
-
// Valid receipt promotes correctly
|
|
2699
|
-
const valid = canonicalReceipt();
|
|
2700
|
-
const good = POLICY.promoteFlashTask(flash, 'PASS', valid);
|
|
2701
|
-
assert.equal(good.readyForSync, true);
|
|
2702
|
-
assert.equal(good.receipt, valid.trim());
|
|
2703
|
-
const forgedFinalize = POLICY.finalizeFlashTask(good, 'sync-finalize');
|
|
2704
|
-
assert.equal(forgedFinalize.status, 'in_progress');
|
|
2705
|
-
assert.equal(forgedFinalize.unblocks, false);
|
|
2706
|
-
const finalized = POLICY.syncFinalizeFlashTask(flash, 'PASS', valid);
|
|
2707
|
-
assert.equal(finalized.status, 'done');
|
|
2708
1598
|
});
|
|
2709
1599
|
|
|
2710
1600
|
test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact receipts compatible', () => {
|
|
@@ -2728,123 +1618,12 @@ test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact
|
|
|
2728
1618
|
}
|
|
2729
1619
|
});
|
|
2730
1620
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
dependencyBlocked: true,
|
|
2738
|
-
unblocks: false,
|
|
2739
|
-
flashTransition: 'promoted',
|
|
2740
|
-
promotionReceipt: receipt,
|
|
2741
|
-
};
|
|
2742
|
-
const direct = spawnSync(process.execPath, [
|
|
2743
|
-
POLICY_PATH,
|
|
2744
|
-
'--sync-finalize',
|
|
2745
|
-
'--task-json',
|
|
2746
|
-
JSON.stringify(forged),
|
|
2747
|
-
'--json',
|
|
2748
|
-
], { encoding: 'utf8' });
|
|
2749
|
-
assert.equal(direct.status, 2, `${direct.stdout}\n${direct.stderr}`);
|
|
2750
|
-
const directPayload = JSON.parse(direct.stdout);
|
|
2751
|
-
assert.equal(directPayload.ok, false);
|
|
2752
|
-
assert.equal(directPayload.task.status, 'in_progress');
|
|
2753
|
-
assert.equal(directPayload.task.receipt, 'FLASH_UNVERIFIED');
|
|
2754
|
-
assert.equal(directPayload.task.readyForSync, false);
|
|
2755
|
-
assert.equal(directPayload.task.dependencyBlocked, true);
|
|
2756
|
-
assert.equal(directPayload.task.unblocks, false);
|
|
2757
|
-
assert.match(directPayload.message, /current task must be FLASH_UNVERIFIED.*canonical/i);
|
|
2758
|
-
|
|
2759
|
-
const minimal = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED' };
|
|
2760
|
-
const minimalResult = POLICY.syncFinalizeFlashTask(minimal, 'PASS', receipt);
|
|
2761
|
-
assert.equal(minimalResult.status, 'in_progress');
|
|
2762
|
-
assert.match(minimalResult.blocker, /exact canonical stored state/);
|
|
2763
|
-
const forgedOpen = { ...canonicalFlashTask(), readyForSync: true };
|
|
2764
|
-
const forgedOpenResult = POLICY.syncFinalizeFlashTask(forgedOpen, 'PASS', receipt);
|
|
2765
|
-
assert.equal(forgedOpenResult.status, 'in_progress');
|
|
2766
|
-
assert.equal(forgedOpenResult.unblocks, false);
|
|
2767
|
-
|
|
2768
|
-
const current = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
|
|
2769
|
-
const supported = spawnSync(process.execPath, [
|
|
2770
|
-
POLICY_PATH,
|
|
2771
|
-
'--sync-finalize',
|
|
2772
|
-
'--task-json',
|
|
2773
|
-
JSON.stringify(current),
|
|
2774
|
-
'--verdict',
|
|
2775
|
-
'PASS',
|
|
2776
|
-
'--proof',
|
|
2777
|
-
receipt,
|
|
2778
|
-
'--project-root',
|
|
2779
|
-
RUNTIME_ROOT,
|
|
2780
|
-
'--specs-root',
|
|
2781
|
-
path.join(RUNTIME_ROOT, 'specs'),
|
|
2782
|
-
'--spec-file',
|
|
2783
|
-
RUNTIME_SPEC,
|
|
2784
|
-
'--feature-name',
|
|
2785
|
-
'demo',
|
|
2786
|
-
'--runtime-session',
|
|
2787
|
-
'implementation-session-1',
|
|
2788
|
-
'--json',
|
|
2789
|
-
], { encoding: 'utf8' });
|
|
2790
|
-
assert.equal(supported.status, 0, `${supported.stdout}\n${supported.stderr}`);
|
|
2791
|
-
assert.equal(JSON.parse(supported.stdout).task.status, 'done');
|
|
2792
|
-
const warningFinalize = POLICY.syncFinalizeFlashTask(current, 'PASS_WITH_WARNINGS', receipt);
|
|
2793
|
-
assert.equal(warningFinalize.status, 'in_progress');
|
|
2794
|
-
assert.equal(warningFinalize.unblocks, false);
|
|
2795
|
-
|
|
2796
|
-
const initial = canonicalFlashTask();
|
|
2797
|
-
const promoted = POLICY.promoteFlashTask(initial, 'PASS', receipt);
|
|
2798
|
-
const persisted = JSON.parse(JSON.stringify(promoted));
|
|
2799
|
-
assert.equal(persisted.flashTransition, 'promoted');
|
|
2800
|
-
assert.equal(persisted.promotionReceipt, receipt.trim());
|
|
2801
|
-
const forgedPersistedFinalize = POLICY.finalizeFlashTask(persisted, 'sync-finalize');
|
|
2802
|
-
assert.equal(forgedPersistedFinalize.status, 'in_progress');
|
|
2803
|
-
assert.equal(forgedPersistedFinalize.unblocks, false);
|
|
2804
|
-
const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', receipt);
|
|
2805
|
-
assert.equal(finalized.status, 'done');
|
|
2806
|
-
assert.equal(finalized.flashTransition, 'finalized');
|
|
2807
|
-
|
|
2808
|
-
const artifactInitial = { ...initial, artifacts: ['output/bundle.js'] };
|
|
2809
|
-
const artifactPromotion = POLICY.promoteFlashTask(artifactInitial, 'PASS', receipt);
|
|
2810
|
-
assert.equal(artifactPromotion.readyForSync, false, 'flash promotion must apply task artifact requirements');
|
|
2811
|
-
|
|
2812
|
-
const badProof = spawnSync(process.execPath, [
|
|
2813
|
-
POLICY_PATH,
|
|
2814
|
-
'--sync-finalize',
|
|
2815
|
-
'--task-json',
|
|
2816
|
-
JSON.stringify(current),
|
|
2817
|
-
'--verdict',
|
|
2818
|
-
'PASS',
|
|
2819
|
-
'--proof',
|
|
2820
|
-
'Verification: PASS',
|
|
2821
|
-
'--json',
|
|
2822
|
-
], { encoding: 'utf8' });
|
|
2823
|
-
assert.equal(badProof.status, 2);
|
|
2824
|
-
assert.equal(JSON.parse(badProof.stdout).task.status, 'in_progress');
|
|
2825
|
-
|
|
2826
|
-
const badArtifact = spawnSync(process.execPath, [
|
|
2827
|
-
POLICY_PATH,
|
|
2828
|
-
'--sync-finalize',
|
|
2829
|
-
'--task-json',
|
|
2830
|
-
JSON.stringify({ ...current, artifacts: ['output/bundle.js'] }),
|
|
2831
|
-
'--verdict',
|
|
2832
|
-
'PASS',
|
|
2833
|
-
'--proof',
|
|
2834
|
-
receipt,
|
|
2835
|
-
'--json',
|
|
2836
|
-
], { encoding: 'utf8' });
|
|
2837
|
-
assert.equal(badArtifact.status, 2);
|
|
2838
|
-
assert.equal(JSON.parse(badArtifact.stdout).task.status, 'in_progress');
|
|
2839
|
-
|
|
2840
|
-
const markerOnly = {
|
|
2841
|
-
...forged,
|
|
2842
|
-
flashTransition: 'promoted',
|
|
2843
|
-
promotionReceipt: 'Verification: PASS',
|
|
2844
|
-
receipt: 'Verification: PASS',
|
|
2845
|
-
};
|
|
2846
|
-
assert.equal(POLICY.finalizeFlashTask(markerOnly, 'sync-finalize').status, 'in_progress');
|
|
2847
|
-
});
|
|
1621
|
+
function writeWorkflowPacketAt(featureDir, taskStatus = 'pending', dependency = 'none') {
|
|
1622
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
1623
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
1624
|
+
fs.writeFileSync(path.join(featureDir, 'task-01-x.md'),
|
|
1625
|
+
`# Task 01\n\nStatus: ${taskStatus}\n\n## Dependencies\n\n- ${dependency}\n\n## Verification Plan\n\n- Command: pnpm test\n`);
|
|
1626
|
+
}
|
|
2848
1627
|
|
|
2849
1628
|
test('P0 Claude and Codex reject configured specs roots outside the project', () => {
|
|
2850
1629
|
const claudeResolver = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
|
|
@@ -2852,15 +1631,11 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
|
|
|
2852
1631
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-external-specs-'));
|
|
2853
1632
|
const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-configured-specs-'));
|
|
2854
1633
|
try {
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
fs.mkdirSync(local, { recursive: true });
|
|
2858
|
-
fs.mkdirSync(external, { recursive: true });
|
|
2859
|
-
fs.writeFileSync(path.join(local, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'local' }));
|
|
2860
|
-
fs.writeFileSync(path.join(external, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'remote' }));
|
|
1634
|
+
writeWorkflowPacketAt(path.join(tmp, 'specs', 'local'));
|
|
1635
|
+
writeWorkflowPacketAt(path.join(externalRoot, 'remote'));
|
|
2861
1636
|
const runtime = { paths: { specs: externalRoot } };
|
|
2862
|
-
const claude = claudeResolver.
|
|
2863
|
-
const codex = codexUtils.
|
|
1637
|
+
const claude = claudeResolver.resolveWorkflowCandidate({ projectRoot: tmp, runtime });
|
|
1638
|
+
const codex = codexUtils.resolveWorkflowCandidate(tmp, runtime, null, null);
|
|
2864
1639
|
assert.equal(claude.error, 'invalid_specs');
|
|
2865
1640
|
assert.equal(codex.error, 'invalid_specs');
|
|
2866
1641
|
assert.match(claude.reason, /escapes project root/);
|
|
@@ -2871,59 +1646,31 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
|
|
|
2871
1646
|
}
|
|
2872
1647
|
});
|
|
2873
1648
|
|
|
2874
|
-
test('P0 caller-owned downgrade flags have no canonical capability', () => {
|
|
2875
|
-
for (const callerClaim of [{ user_approved: true }, { userApproved: true }, { userAuthorized: true }]) {
|
|
2876
|
-
assert.throws(
|
|
2877
|
-
() => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct', ...callerClaim }),
|
|
2878
|
-
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2879
|
-
);
|
|
2880
|
-
}
|
|
2881
|
-
assert.throws(() => POLICY.classifyLane({ riskSignals: { auth: true }, override: 'Standard', confirmDowngrade: true }), /assurance_level downgrade.*blocked/i);
|
|
2882
|
-
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
2883
|
-
});
|
|
2884
|
-
|
|
2885
|
-
test('P0 canonical policy exposes no caller-owned downgrade authority', () => {
|
|
2886
|
-
assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
|
|
2887
|
-
assert.equal(POLICY.isValidOverrideReceipt, undefined);
|
|
2888
|
-
assert.equal(POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS.includes('override_' + 'receipt'), false);
|
|
2889
|
-
assert.throws(
|
|
2890
|
-
() => POLICY.classifyLane({
|
|
2891
|
-
riskSignals: { privacy: true },
|
|
2892
|
-
override: 'Direct',
|
|
2893
|
-
overrideReceipt: { verifiedByRuntime: true },
|
|
2894
|
-
}),
|
|
2895
|
-
(error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
|
|
2896
|
-
);
|
|
2897
|
-
});
|
|
2898
|
-
|
|
2899
1649
|
test('P0 active spec deterministic: multiple active ambiguity and explicit target/path containment', () => {
|
|
2900
1650
|
const RESOLVER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
|
|
2901
1651
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-'));
|
|
2902
1652
|
try {
|
|
2903
1653
|
const specsDir = path.join(tmp, 'specs');
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
for (const name of ['alpha', 'beta']) {
|
|
2907
|
-
fs.mkdirSync(path.join(specsDir, name), { recursive: true });
|
|
2908
|
-
fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name, current_phase: 'design' }));
|
|
2909
|
-
}
|
|
2910
|
-
const amb = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
|
|
1654
|
+
for (const name of ['alpha', 'beta']) writeWorkflowPacketAt(path.join(specsDir, name));
|
|
1655
|
+
const amb = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
2911
1656
|
assert.equal(amb.error, 'multiple_active');
|
|
2912
1657
|
assert.deepEqual(amb.candidates.sort(), ['alpha', 'beta']);
|
|
2913
1658
|
// Explicit feature resolves deterministically
|
|
2914
|
-
const alpha = RESOLVER.
|
|
1659
|
+
const alpha = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'alpha' });
|
|
2915
1660
|
assert.equal(alpha.featureName, 'alpha');
|
|
2916
1661
|
// Explicit not-found fail-closed
|
|
2917
|
-
const notFound = RESOLVER.
|
|
1662
|
+
const notFound = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'gamma' });
|
|
2918
1663
|
assert.equal(notFound.error, 'explicit_not_found');
|
|
2919
|
-
// Explicit path containment:
|
|
2920
|
-
|
|
1664
|
+
// Explicit path containment: a plan.md reached through ../ is outside the specs root
|
|
1665
|
+
fs.mkdirSync(path.join(tmp, 'etc'), { recursive: true });
|
|
1666
|
+
fs.writeFileSync(path.join(tmp, 'etc', 'plan.md'), '# Outside\n');
|
|
1667
|
+
const escape = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(tmp, 'specs', '..', 'etc', 'plan.md') });
|
|
2921
1668
|
assert.equal(escape.error, 'explicit_malformed');
|
|
2922
1669
|
// Malformed feature name with slash must be rejected
|
|
2923
|
-
const malformed = RESOLVER.
|
|
1670
|
+
const malformed = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: '../alpha' });
|
|
2924
1671
|
assert.equal(malformed.error, 'explicit_malformed');
|
|
2925
1672
|
// Valid explicit path inside root
|
|
2926
|
-
const validPath = RESOLVER.
|
|
1673
|
+
const validPath = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'beta', 'plan.md') });
|
|
2927
1674
|
assert.equal(validPath.featureName, 'beta');
|
|
2928
1675
|
} finally {
|
|
2929
1676
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
@@ -2942,30 +1689,23 @@ test('P0 parity Claude/Codex: canonical receipt and multi-active', () => {
|
|
|
2942
1689
|
const codexUtils = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
|
|
2943
1690
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-resolver-'));
|
|
2944
1691
|
try {
|
|
2945
|
-
const
|
|
2946
|
-
|
|
2947
|
-
for (const name of ['x', 'y']) {
|
|
2948
|
-
fs.mkdirSync(path.join(specsDir, name), { recursive: true });
|
|
2949
|
-
fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name }));
|
|
2950
|
-
}
|
|
2951
|
-
const amb = codexUtils.resolveActiveSpec(tmp, {}, null, null);
|
|
1692
|
+
for (const name of ['x', 'y']) writeWorkflowPacketAt(path.join(tmp, 'specs', name));
|
|
1693
|
+
const amb = codexUtils.resolveWorkflowCandidate(tmp, {}, null, null);
|
|
2952
1694
|
assert.equal(amb.error, 'multiple_active');
|
|
2953
|
-
|
|
1695
|
+
fs.mkdirSync(path.join(tmp, 'outside'), { recursive: true });
|
|
1696
|
+
fs.writeFileSync(path.join(tmp, 'outside', 'plan.md'), '# Outside\n');
|
|
1697
|
+
const esc = codexUtils.resolveWorkflowCandidate(tmp, {}, null, path.join(tmp, 'specs', '..', 'outside', 'plan.md'));
|
|
2954
1698
|
assert.equal(esc.error, 'explicit_malformed');
|
|
2955
1699
|
} finally {
|
|
2956
1700
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
2957
1701
|
}
|
|
2958
1702
|
});
|
|
2959
1703
|
|
|
2960
|
-
test('P0 regression: placeholder, explicit failure
|
|
2961
|
-
// Probe A: explicit failure outcomes must fail canonical validator
|
|
1704
|
+
test('P0 regression: placeholder, explicit failure and artifact receipts are blocked', () => {
|
|
1705
|
+
// Probe A: explicit failure outcomes must fail the canonical validator
|
|
2962
1706
|
const bodyTestsFailed = 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nTests failed: 1\n';
|
|
2963
1707
|
assert.ok(POLICY.validateCanonicalReceipt(bodyTestsFailed).length > 0, 'Tests failed: 1 must be rejected');
|
|
2964
1708
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nResult: FAIL\nResult: PASS\n').includes('exit_result'), 'Result FAIL then PASS must be rejected');
|
|
2965
|
-
const flashTask = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED', blocker: 'awaiting', dependencyBlocked: true, unblocks: false };
|
|
2966
|
-
const notPromoted = POLICY.promoteFlashTask(flashTask, 'PASS', bodyTestsFailed);
|
|
2967
|
-
assert.equal(notPromoted.readyForSync, false, 'flash with Tests failed should not promote');
|
|
2968
|
-
assert.equal(notPromoted.receipt, 'FLASH_UNVERIFIED');
|
|
2969
1709
|
|
|
2970
1710
|
// Probe B: placeholder tokens must be rejected, but substring todo must pass
|
|
2971
1711
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: TODO\nExit: 0\nBase: a\nHead: b\n').includes('command'), 'Command TODO must be rejected');
|
|
@@ -2980,24 +1720,6 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
|
|
|
2980
1720
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: bundle\nsha256: abc123\n').includes('artifact_hash'), 'short artifact sha must fail');
|
|
2981
1721
|
assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact produced sha256:deadbeef\n').includes('artifact_hash'), 'short inline sha256 must fail');
|
|
2982
1722
|
assert.deepEqual(POLICY.validateCanonicalReceipt(`Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\nArtifact: bundle\nsha256: ${'c'.repeat(64)}\n`), [], '64-hex artifact sha should pass');
|
|
2983
|
-
|
|
2984
|
-
// Probe F: lanePolicy forged plain object must not bypass classification
|
|
2985
|
-
const { spawnSync } = require('node:child_process');
|
|
2986
|
-
const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
|
|
2987
|
-
const forged = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct', riskSignals: { auth: true } }), '--json'], { encoding: 'utf8' });
|
|
2988
|
-
assert.equal(forged.status, 2, 'forged Direct with auth should be blocked via lanePolicy');
|
|
2989
|
-
assert.match(forged.stderr, /downgrade.*not permitted/i);
|
|
2990
|
-
const forged2 = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct' }), '--json'], { encoding: 'utf8' });
|
|
2991
|
-
assert.equal(forged2.status, 2, 'plain Direct without justification should be blocked');
|
|
2992
|
-
|
|
2993
|
-
const trusted = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true });
|
|
2994
|
-
assert.equal(trusted.lane, 'Direct');
|
|
2995
|
-
const viaPolicy = POLICY.lanePolicy(trusted);
|
|
2996
|
-
assert.equal(viaPolicy.lane, 'Direct', 'trusted Direct should be respected');
|
|
2997
|
-
assert.throws(() => { 'use strict'; trusted.lane = 'Direct'; }, /read only|Cannot assign|TypeError/);
|
|
2998
|
-
const paymentTrusted = POLICY.classifyLane({ riskSignals: { payment: true } });
|
|
2999
|
-
const forgedCopy = { ...paymentTrusted, lane: 'Direct' };
|
|
3000
|
-
assert.equal(POLICY.lanePolicy(forgedCopy).lane, 'Standard', 'compatibility lane is derived from authoritative axes');
|
|
3001
1723
|
});
|
|
3002
1724
|
|
|
3003
1725
|
test('P0 regression: symlink spec/task containment and malformed spec handling', () => {
|
|
@@ -3005,33 +1727,29 @@ test('P0 regression: symlink spec/task containment and malformed spec handling',
|
|
|
3005
1727
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-regression-symlink-'));
|
|
3006
1728
|
try {
|
|
3007
1729
|
const specsDir = path.join(tmp, 'specs');
|
|
3008
|
-
|
|
3009
|
-
fs.mkdirSync(path.join(specsDir, 'valid'), { recursive: true });
|
|
3010
|
-
fs.writeFileSync(path.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'valid' }));
|
|
1730
|
+
writeWorkflowPacketAt(path.join(specsDir, 'valid'));
|
|
3011
1731
|
const outside = path.join(tmp, 'outside');
|
|
3012
|
-
|
|
3013
|
-
fs.
|
|
3014
|
-
const
|
|
3015
|
-
fs.symlinkSync(outside, link);
|
|
3016
|
-
const e1 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'linked' });
|
|
1732
|
+
writeWorkflowPacketAt(outside);
|
|
1733
|
+
fs.symlinkSync(outside, path.join(specsDir, 'linked'));
|
|
1734
|
+
const e1 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'linked' });
|
|
3017
1735
|
assert.equal(e1.error, 'explicit_malformed');
|
|
3018
|
-
const e2 = RESOLVER.
|
|
1736
|
+
const e2 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'linked', 'plan.md') });
|
|
3019
1737
|
assert.equal(e2.error, 'explicit_malformed');
|
|
3020
|
-
const nonExplicit = RESOLVER.
|
|
1738
|
+
const nonExplicit = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3021
1739
|
assert.equal(nonExplicit.error, 'invalid_specs');
|
|
3022
1740
|
assert.ok(nonExplicit.candidates.includes('linked'));
|
|
3023
|
-
|
|
3024
|
-
|
|
1741
|
+
fs.rmSync(path.join(specsDir, 'linked'));
|
|
1742
|
+
// A task file that is a symlink to a file outside the packet is refused.
|
|
3025
1743
|
const taskOutside = path.join(tmp, 'outside-task.md');
|
|
3026
|
-
fs.
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
const mal = RESOLVER.
|
|
1744
|
+
fs.copyFileSync(path.join(specsDir, 'valid', 'task-01-x.md'), taskOutside);
|
|
1745
|
+
fs.symlinkSync(taskOutside, path.join(specsDir, 'valid', 'task-02-link.md'));
|
|
1746
|
+
const linkedTask = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
1747
|
+
assert.equal(linkedTask.error, 'invalid_specs');
|
|
1748
|
+
assert.ok(linkedTask.candidates.includes('valid'));
|
|
1749
|
+
fs.rmSync(path.join(specsDir, 'valid', 'task-02-link.md'));
|
|
1750
|
+
// A malformed packet (dependency on a missing task) is reported, not skipped.
|
|
1751
|
+
writeWorkflowPacketAt(path.join(specsDir, 'bad'), 'pending', 'task-09-missing.md');
|
|
1752
|
+
const mal = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3035
1753
|
assert.equal(mal.error, 'invalid_specs');
|
|
3036
1754
|
assert.ok(mal.candidates.includes('bad'));
|
|
3037
1755
|
} finally {
|
|
@@ -3102,18 +1820,18 @@ test('P0 regression: safeTaskFile fail-closed on realpath error and single autho
|
|
|
3102
1820
|
assert.match(codexSrc, /getSharedValidate/);
|
|
3103
1821
|
assert.match(codexSrc, /workflow-policy\.cjs/);
|
|
3104
1822
|
assert.doesNotMatch(codexSrc, /const PLACEHOLDER_TOKENS/);
|
|
3105
|
-
//
|
|
1823
|
+
// The task-file read fails closed when realpath throws
|
|
3106
1824
|
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-realpath-'));
|
|
3107
1825
|
try {
|
|
3108
1826
|
const featDir = path2.join(tmp, 'feat');
|
|
3109
|
-
|
|
3110
|
-
fs2.
|
|
1827
|
+
writeWorkflowPacketAt(featDir, 'done');
|
|
1828
|
+
fs2.appendFileSync(path2.join(featDir, 'task-01-x.md'), '\n## Receipt\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n```\npnpm test\n```\n');
|
|
1829
|
+
const codexReceipt = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'));
|
|
3111
1830
|
const orig = fs2.realpathSync;
|
|
3112
1831
|
fs2.realpathSync = () => { throw new Error('simulated realpath failure'); };
|
|
3113
1832
|
try {
|
|
3114
|
-
const
|
|
3115
|
-
|
|
3116
|
-
assert.ok(fails.includes('a'), 'realpath failure should be fail-closed with check a');
|
|
1833
|
+
const proof = codexReceipt.checkWorkflowReceiptDetails(featDir, 'task-01-x.md', null);
|
|
1834
|
+
assert.ok(proof.failures.includes('unsafe_path'), `realpath failure must fail closed, got ${proof.failures}`);
|
|
3117
1835
|
} finally {
|
|
3118
1836
|
fs2.realpathSync = orig;
|
|
3119
1837
|
}
|
|
@@ -3130,15 +1848,12 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3130
1848
|
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-resolver-fail-'));
|
|
3131
1849
|
try {
|
|
3132
1850
|
const specsDir = path2.join(tmp, 'specs');
|
|
3133
|
-
|
|
3134
|
-
fs2.writeFileSync(path2.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo' }));
|
|
3135
|
-
// Monkey patch realpathSync to always throw for explicitFeature
|
|
1851
|
+
writeWorkflowPacketAt(path2.join(specsDir, 'demo'));
|
|
3136
1852
|
const origRealpath = fs2.realpathSync;
|
|
3137
1853
|
fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
|
|
3138
1854
|
try {
|
|
3139
|
-
const res = RESOLVER.
|
|
1855
|
+
const res = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'demo' });
|
|
3140
1856
|
assert.equal(res.error, 'explicit_malformed', 'explicitFeature realpath failure should be explicit_malformed');
|
|
3141
|
-
assert.match(res.reason, /canonicalization error/);
|
|
3142
1857
|
} finally {
|
|
3143
1858
|
fs2.realpathSync = origRealpath;
|
|
3144
1859
|
}
|
|
@@ -3149,7 +1864,7 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3149
1864
|
return origRealpath2(p);
|
|
3150
1865
|
};
|
|
3151
1866
|
try {
|
|
3152
|
-
const res2 = RESOLVER.
|
|
1867
|
+
const res2 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3153
1868
|
assert.equal(res2.error, 'invalid_specs');
|
|
3154
1869
|
assert.ok(res2.candidates.includes('<specs>'));
|
|
3155
1870
|
} finally {
|
|
@@ -3162,21 +1877,17 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3162
1877
|
return origLstat(p);
|
|
3163
1878
|
};
|
|
3164
1879
|
try {
|
|
3165
|
-
const res3 = RESOLVER.
|
|
1880
|
+
const res3 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3166
1881
|
assert.equal(res3.error, 'invalid_specs');
|
|
3167
1882
|
assert.ok(res3.candidates.includes('demo'));
|
|
3168
1883
|
} finally {
|
|
3169
1884
|
fs2.lstatSync = origLstat;
|
|
3170
1885
|
}
|
|
3171
|
-
//
|
|
3172
|
-
const outside = path2.join(tmp, 'outside-spec2');
|
|
3173
|
-
fs2.mkdirSync(outside, { recursive: true });
|
|
3174
|
-
fs2.writeFileSync(path2.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
|
|
1886
|
+
// A dangling plan.md symlink is invalid
|
|
3175
1887
|
const featDir = path2.join(specsDir, 'dangle');
|
|
3176
1888
|
fs2.mkdirSync(featDir, { recursive: true });
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
const res4 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
|
|
1889
|
+
fs2.symlinkSync(path2.join(tmp, 'nonexistent-target'), path2.join(featDir, 'plan.md'));
|
|
1890
|
+
const res4 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
|
|
3180
1891
|
assert.equal(res4.error, 'invalid_specs');
|
|
3181
1892
|
assert.ok(res4.candidates.includes('dangle'));
|
|
3182
1893
|
} finally {
|
|
@@ -3186,13 +1897,11 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
|
|
|
3186
1897
|
const codexUtils = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
|
|
3187
1898
|
const tmp2 = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-fail-'));
|
|
3188
1899
|
try {
|
|
3189
|
-
|
|
3190
|
-
fs2.mkdirSync(path2.join(specsDir2, 'demo2'), { recursive: true });
|
|
3191
|
-
fs2.writeFileSync(path2.join(specsDir2, 'demo2', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo2' }));
|
|
1900
|
+
writeWorkflowPacketAt(path2.join(tmp2, 'specs', 'demo2'));
|
|
3192
1901
|
const orig = fs2.realpathSync;
|
|
3193
1902
|
fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
|
|
3194
1903
|
try {
|
|
3195
|
-
const r = codexUtils.
|
|
1904
|
+
const r = codexUtils.resolveWorkflowCandidate(tmp2, {}, 'demo2', null);
|
|
3196
1905
|
assert.equal(r.error, 'explicit_malformed');
|
|
3197
1906
|
} finally {
|
|
3198
1907
|
fs2.realpathSync = orig;
|