@boyingliu01/xp-gate 0.18.5 → 0.19.0
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/adapter-common.sh +27 -0
- package/adapters/gate-3.sh +0 -0
- package/adapters/gate-4.sh +0 -0
- package/adapters/gate-7.sh +0 -0
- package/adapters/gate-8.sh +0 -0
- package/adapters/gate-9.sh +0 -0
- package/adapters/powershell.sh +85 -33
- package/adapters/typescript.sh +8 -8
- package/build-integrity/__tests__/import-resolver.test.ts +22 -0
- package/build-integrity/__tests__/orchestrator.test.ts +1 -2
- package/build-integrity/gate-10.ts +7 -0
- package/gate-12-file-hygiene.sh +230 -0
- package/hooks/adapter-common.sh +27 -0
- package/hooks/gate-10.sh +105 -0
- package/hooks/gate-12-file-hygiene.sh +0 -0
- package/hooks/lib/validate-code-walkthrough.cjs +125 -0
- package/hooks/pre-commit +130 -33
- package/hooks/pre-push +15 -91
- package/lib/__tests__/check-version.test.js +67 -46
- package/lib/__tests__/doctor.test.js +7 -6
- package/lib/__tests__/gate-runner.test.js +267 -0
- package/lib/__tests__/init.test.js +32 -0
- package/lib/__tests__/npm-package.test.js +13 -0
- package/lib/__tests__/phase-transition.test.js +604 -12
- package/lib/__tests__/sprint-discovery.test.ts +0 -1
- package/lib/__tests__/sprint-status-rework.test.js +100 -1
- package/lib/__tests__/update-hooks.test.js +143 -10
- package/lib/__tests__/upgrade-exec.test.js +73 -45
- package/lib/__tests__/upgrade-skill-sync.test.js +1 -1
- package/lib/__tests__/upgrade.test.js +12 -11
- package/lib/doctor.js +2 -1
- package/lib/gate-runner.js +68 -23
- package/lib/init.js +34 -5
- package/lib/install-skill.js +1 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/phase-transition.js +203 -23
- package/lib/shared-paths.js +2 -0
- package/lib/sprint-status.js +15 -4
- package/lib/test-alignment.ts +3 -13
- package/lib/update-hooks.js +129 -15
- package/mock-policy/AGENTS.md +5 -5
- package/mock-policy/__tests__/gate-m3.test.ts +0 -5
- package/mock-policy/__tests__/scope-scanner.test.ts +8 -2
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/mutmut-runner.test.ts +443 -2
- package/mutation/__tests__/update-baseline.test.ts +0 -2
- package/mutation/runners/mutmut-runner.ts +67 -17
- package/package.json +9 -3
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/claude-code/skills/delphi-review/.delphi-config.json.example +24 -27
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +13 -12
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +15 -17
- package/plugins/claude-code/skills/delphi-review/SKILL.md +30 -29
- package/plugins/claude-code/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +66 -54
- package/plugins/claude-code/skills/delphi-review/references/round-templates.md +7 -7
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +37 -35
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +24 -14
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +25 -0
- package/plugins/dsh/cordis.patch.yml +7 -0
- package/plugins/dsh/package.json +70 -0
- package/plugins/dsh/scripts/prepack.cjs +82 -0
- package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +113 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/plugins/dsh/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/plugins/dsh/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/dsh/skills/delphi-review/.delphi-config.json.example +70 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +67 -0
- package/plugins/dsh/skills/delphi-review/INSTALL.md +150 -0
- package/plugins/dsh/skills/delphi-review/SKILL.md +559 -0
- package/plugins/dsh/skills/delphi-review/hooks.md +19 -0
- package/plugins/dsh/skills/delphi-review/opencode.json.delphi.example +52 -0
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +470 -0
- package/plugins/dsh/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/plugins/dsh/skills/delphi-review/references/requirements.md +402 -0
- package/plugins/dsh/skills/delphi-review/references/round-templates.md +134 -0
- package/plugins/dsh/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/dsh/skills/delphi-review/tools.md +29 -0
- package/plugins/dsh/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/dsh/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/dsh/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/dsh/skills/grilling/SKILL.md +12 -0
- package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/dsh/skills/ralph-loop/SKILL.md +198 -0
- package/plugins/dsh/skills/ralph-loop/references/components.md +113 -0
- package/plugins/dsh/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/dsh/skills/ralph-loop/references/workflow-details.md +99 -0
- package/plugins/dsh/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +103 -0
- package/plugins/dsh/skills/sprint-flow/SKILL.md +411 -0
- package/plugins/dsh/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
- package/plugins/dsh/skills/sprint-flow/hooks.md +18 -0
- package/plugins/dsh/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/dsh/skills/sprint-flow/references/components/middleware.md +75 -0
- package/plugins/dsh/skills/sprint-flow/references/components/skill-invocations.md +105 -0
- package/plugins/dsh/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/dsh/skills/sprint-flow/references/components/tool-descriptions.md +95 -0
- package/plugins/dsh/skills/sprint-flow/references/force-levels.md +204 -0
- package/plugins/dsh/skills/sprint-flow/references/orchestration-rules.md +236 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-1-prep.md +294 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +273 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-3-build.md +348 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +225 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +222 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-6-close.md +266 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-overview.md +486 -0
- package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-output-template.md +110 -0
- package/plugins/dsh/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/dsh/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/dsh/skills/sprint-flow/templates/sprint-progress-template.md +140 -0
- package/plugins/dsh/skills/sprint-flow/templates/sprint-summary-template.md +121 -0
- package/plugins/dsh/skills/sprint-flow/tools.md +29 -0
- package/plugins/dsh/skills/test-driven-development/SKILL.md +371 -0
- package/plugins/dsh/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/dsh/skills/test-specification-alignment/SKILL.md +205 -0
- package/plugins/dsh/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/dsh/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/dsh/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/dsh/skills/to-issues/SKILL.md +277 -0
- package/plugins/dsh/src/command.test.ts +102 -0
- package/plugins/dsh/src/command.ts +75 -0
- package/plugins/dsh/src/contract.test.ts +62 -0
- package/plugins/dsh/src/gate-runner.test.ts +100 -0
- package/plugins/dsh/src/gate-runner.ts +58 -0
- package/plugins/dsh/src/index.test.ts +97 -0
- package/plugins/dsh/src/index.ts +94 -0
- package/plugins/dsh/src/skills.test.ts +58 -0
- package/plugins/dsh/tsconfig.build.json +13 -0
- package/plugins/dsh/tsconfig.json +19 -0
- package/plugins/dsh/vitest.config.ts +8 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/scripts/prepack.cjs +1 -0
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/opencode/skills/delphi-review/.delphi-config.json.example +24 -27
- package/plugins/opencode/skills/delphi-review/AGENTS.md +13 -12
- package/plugins/opencode/skills/delphi-review/INSTALL.md +15 -17
- package/plugins/opencode/skills/delphi-review/SKILL.md +30 -29
- package/plugins/opencode/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/plugins/opencode/skills/delphi-review/references/requirements.md +66 -54
- package/plugins/opencode/skills/delphi-review/references/round-templates.md +7 -7
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/opencode/skills/sprint-flow/SKILL.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +2 -2
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +37 -35
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +24 -14
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +6 -6
- package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/clipboard-vision/SKILL.md +47 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/SKILL.md +47 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +3 -11
- package/plugins/qoder/skills/delphi-review/AGENTS.md +15 -17
- package/plugins/qoder/skills/delphi-review/INSTALL.md +12 -12
- package/plugins/qoder/skills/delphi-review/SKILL.md +58 -25
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +1 -1
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/qoder/skills/sprint-flow/SKILL.md +2 -2
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +38 -35
- package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +48 -6
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +5 -6
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/scripts/__tests__/sync-package-content.test.js +30 -0
- package/scripts/delphi-external-review.cjs +194 -63
- package/scripts/sync-package-content.js +9 -2
- package/skills/clipboard-vision/SKILL.md +47 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
- package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
- package/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
- package/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
- package/skills/delphi-review/.delphi-config.json.example +24 -27
- package/skills/delphi-review/AGENTS.md +13 -12
- package/skills/delphi-review/INSTALL.md +15 -17
- package/skills/delphi-review/SKILL.md +30 -29
- package/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/skills/delphi-review/references/code-walkthrough.md +33 -49
- package/skills/delphi-review/references/requirements.md +66 -54
- package/skills/delphi-review/references/round-templates.md +7 -7
- package/skills/sprint-flow/AGENTS.md +5 -5
- package/skills/sprint-flow/SKILL.md +4 -4
- package/skills/sprint-flow/references/components/skill-invocations.md +2 -2
- package/skills/sprint-flow/references/force-levels.md +37 -35
- package/skills/sprint-flow/references/orchestration-rules.md +1 -1
- package/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/skills/sprint-flow/references/phase-2-design.md +24 -14
- package/skills/sprint-flow/references/phase-4-verify.md +6 -6
- package/skills/sprint-flow/references/phase-overview.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/sprint-gate.sh +291 -0
|
@@ -9,10 +9,49 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import crypto from 'node:crypto';
|
|
12
14
|
import fs from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
15
|
import os from 'node:os';
|
|
15
|
-
import
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import {
|
|
18
|
+
handlePhaseTransition,
|
|
19
|
+
renderDashboard,
|
|
20
|
+
PHASE_NAMES,
|
|
21
|
+
validateEvidence,
|
|
22
|
+
checkWalkthrough,
|
|
23
|
+
checkBypassAudit,
|
|
24
|
+
} from '../phase-transition.js';
|
|
25
|
+
|
|
26
|
+
function git(command, cwd) {
|
|
27
|
+
const env = { ...process.env };
|
|
28
|
+
for (const name of [
|
|
29
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS',
|
|
30
|
+
'GIT_CONFIG_COUNT', 'GIT_OBJECT_DIRECTORY', 'GIT_DIR', 'GIT_WORK_TREE',
|
|
31
|
+
'GIT_IMPLICIT_WORK_TREE', 'GIT_GRAFT_FILE', 'GIT_INDEX_FILE',
|
|
32
|
+
'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_PREFIX',
|
|
33
|
+
'GIT_SHALLOW_FILE', 'GIT_COMMON_DIR',
|
|
34
|
+
]) {
|
|
35
|
+
delete env[name];
|
|
36
|
+
}
|
|
37
|
+
return execSync(`git ${command}`, {
|
|
38
|
+
cwd,
|
|
39
|
+
env,
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
|
+
}).trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createRepository(dir, message) {
|
|
46
|
+
git('init --quiet --template=', dir);
|
|
47
|
+
git('config core.hooksPath /dev/null', dir);
|
|
48
|
+
git('config user.email "test@test.com"', dir);
|
|
49
|
+
git('config user.name "Test"', dir);
|
|
50
|
+
fs.writeFileSync(path.join(dir, 'file.txt'), message);
|
|
51
|
+
git('add file.txt', dir);
|
|
52
|
+
git(`commit --quiet -m "${message}"`, dir);
|
|
53
|
+
return git('rev-parse HEAD', dir);
|
|
54
|
+
}
|
|
16
55
|
|
|
17
56
|
describe('phase-transition', () => {
|
|
18
57
|
let tmpDir;
|
|
@@ -27,22 +66,42 @@ describe('phase-transition', () => {
|
|
|
27
66
|
|
|
28
67
|
/** Helper: write phase 2 evidence (requirements-reviewed.json) so phase 2 completed passes */
|
|
29
68
|
function writePhase2Evidence(dir) {
|
|
69
|
+
const headCommit = fs.existsSync(path.join(dir, '.git'))
|
|
70
|
+
? git('rev-parse HEAD', dir)
|
|
71
|
+
: createRepository(dir, 'phase 2 evidence');
|
|
72
|
+
const requirementsStatement = 'Phase 2 evidence fixture';
|
|
73
|
+
const timestamp = new Date().toISOString();
|
|
74
|
+
const requirementsHash = crypto.createHash('sha256')
|
|
75
|
+
.update(`${requirementsStatement}${timestamp.slice(0, 10)}`, 'utf8')
|
|
76
|
+
.digest('hex');
|
|
30
77
|
const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
|
|
31
78
|
fs.mkdirSync(outputsDir, { recursive: true });
|
|
32
79
|
fs.writeFileSync(path.join(outputsDir, 'requirements-reviewed.json'), JSON.stringify({
|
|
33
80
|
verdict: 'APPROVED',
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
requirements_statement: requirementsStatement,
|
|
82
|
+
context_file_used: null,
|
|
83
|
+
requirements_hash: requirementsHash,
|
|
84
|
+
head_commit: headCommit,
|
|
85
|
+
consensus_ratio: 0.9,
|
|
86
|
+
expert_verdicts: [
|
|
87
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
88
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
89
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
90
|
+
],
|
|
91
|
+
timestamp,
|
|
36
92
|
}));
|
|
37
93
|
}
|
|
38
94
|
|
|
39
95
|
/** Helper: write phase 4 evidence (test-alignment-report.json) so phase 4 completed passes */
|
|
40
96
|
function writePhase4Evidence(dir) {
|
|
97
|
+
const headCommit = fs.existsSync(path.join(dir, '.git'))
|
|
98
|
+
? git('rev-parse HEAD', dir)
|
|
99
|
+
: createRepository(dir, 'phase 4 evidence');
|
|
41
100
|
const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
|
|
42
101
|
fs.mkdirSync(outputsDir, { recursive: true });
|
|
43
102
|
fs.writeFileSync(path.join(outputsDir, 'test-alignment-report.json'), JSON.stringify({
|
|
44
103
|
alignment_status: 'PASS',
|
|
45
|
-
head_commit:
|
|
104
|
+
head_commit: headCommit,
|
|
46
105
|
spec_hash: null,
|
|
47
106
|
timestamp: new Date().toISOString(),
|
|
48
107
|
}));
|
|
@@ -196,6 +255,138 @@ describe('phase-transition', () => {
|
|
|
196
255
|
expect(result).toContain('sprint-min');
|
|
197
256
|
});
|
|
198
257
|
});
|
|
258
|
+
|
|
259
|
+
describe('repository-scoped Git checks', () => {
|
|
260
|
+
it('checks walkthrough ancestry in projectDir when hook Git variables point elsewhere', () => {
|
|
261
|
+
const outerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-outer-'));
|
|
262
|
+
const targetCommit = createRepository(tmpDir, 'target commit');
|
|
263
|
+
createRepository(outerDir, 'outer commit');
|
|
264
|
+
fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
|
|
265
|
+
verdict: 'APPROVED',
|
|
266
|
+
commit: targetCommit,
|
|
267
|
+
expires: new Date(Date.now() + 3600000).toISOString(),
|
|
268
|
+
}));
|
|
269
|
+
const previousGitDir = process.env.GIT_DIR;
|
|
270
|
+
const previousWorkTree = process.env.GIT_WORK_TREE;
|
|
271
|
+
process.env.GIT_DIR = path.join(outerDir, '.git');
|
|
272
|
+
process.env.GIT_WORK_TREE = outerDir;
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
expect(checkWalkthrough(tmpDir).ok).toBe(true);
|
|
276
|
+
} finally {
|
|
277
|
+
if (previousGitDir === undefined) delete process.env.GIT_DIR;
|
|
278
|
+
else process.env.GIT_DIR = previousGitDir;
|
|
279
|
+
if (previousWorkTree === undefined) delete process.env.GIT_WORK_TREE;
|
|
280
|
+
else process.env.GIT_WORK_TREE = previousWorkTree;
|
|
281
|
+
fs.rmSync(outerDir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('rejects a walkthrough bound to an older ancestor commit', () => {
|
|
286
|
+
const reviewedCommit = createRepository(tmpDir, 'reviewed commit');
|
|
287
|
+
fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'new unreviewed change');
|
|
288
|
+
git('add file.txt', tmpDir);
|
|
289
|
+
git('commit --quiet -m "unreviewed commit"', tmpDir);
|
|
290
|
+
fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
|
|
291
|
+
verdict: 'APPROVED',
|
|
292
|
+
commit: reviewedCommit,
|
|
293
|
+
expires: new Date(Date.now() + 3600000).toISOString(),
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
expect(checkWalkthrough(tmpDir).ok).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('accepts a walkthrough bound to the current HEAD commit', () => {
|
|
300
|
+
const reviewedCommit = createRepository(tmpDir, 'reviewed commit');
|
|
301
|
+
fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
|
|
302
|
+
verdict: 'APPROVED',
|
|
303
|
+
commit: reviewedCommit,
|
|
304
|
+
expires: new Date(Date.now() + 3600000).toISOString(),
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
expect(checkWalkthrough(tmpDir).ok).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('checks bypass commits in projectDir when hook Git variables point elsewhere', () => {
|
|
311
|
+
const outerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-outer-'));
|
|
312
|
+
const targetCommit = createRepository(tmpDir, 'target bypass');
|
|
313
|
+
createRepository(outerDir, 'outer commit');
|
|
314
|
+
const auditDir = path.join(tmpDir, '.xp-gate');
|
|
315
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
316
|
+
fs.writeFileSync(path.join(auditDir, 'bypass-audit.jsonl'), JSON.stringify({
|
|
317
|
+
type: 'precommit_bypass',
|
|
318
|
+
commit: targetCommit,
|
|
319
|
+
}));
|
|
320
|
+
const previousGitDir = process.env.GIT_DIR;
|
|
321
|
+
const previousWorkTree = process.env.GIT_WORK_TREE;
|
|
322
|
+
process.env.GIT_DIR = path.join(outerDir, '.git');
|
|
323
|
+
process.env.GIT_WORK_TREE = outerDir;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const result = checkBypassAudit(tmpDir);
|
|
327
|
+
expect(result.ok).toBe(false);
|
|
328
|
+
expect(result.bypassedCommits).toEqual([targetCommit]);
|
|
329
|
+
} finally {
|
|
330
|
+
if (previousGitDir === undefined) delete process.env.GIT_DIR;
|
|
331
|
+
else process.env.GIT_DIR = previousGitDir;
|
|
332
|
+
if (previousWorkTree === undefined) delete process.env.GIT_WORK_TREE;
|
|
333
|
+
else process.env.GIT_WORK_TREE = previousWorkTree;
|
|
334
|
+
fs.rmSync(outerDir, { recursive: true, force: true });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('does not execute shell syntax from a walkthrough commit value', () => {
|
|
339
|
+
const targetCommit = createRepository(tmpDir, 'target commit');
|
|
340
|
+
const markerFile = path.join(tmpDir, 'walkthrough-injection-marker');
|
|
341
|
+
fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
|
|
342
|
+
verdict: 'APPROVED',
|
|
343
|
+
commit: `${targetCommit}; touch ${markerFile}; #`,
|
|
344
|
+
expires: new Date(Date.now() + 3600000).toISOString(),
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
const result = checkWalkthrough(tmpDir);
|
|
348
|
+
|
|
349
|
+
expect(result.ok).toBe(false);
|
|
350
|
+
expect(fs.existsSync(markerFile)).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it.each([
|
|
354
|
+
['missing', undefined],
|
|
355
|
+
['null', null],
|
|
356
|
+
['numeric', 123],
|
|
357
|
+
['empty', ''],
|
|
358
|
+
['malformed', 'not-a-commit'],
|
|
359
|
+
])('rejects a %s walkthrough commit', (_label, commit) => {
|
|
360
|
+
createRepository(tmpDir, 'target commit');
|
|
361
|
+
const walkthrough = {
|
|
362
|
+
verdict: 'APPROVED',
|
|
363
|
+
expires: new Date(Date.now() + 3600000).toISOString(),
|
|
364
|
+
};
|
|
365
|
+
if (commit !== undefined) walkthrough.commit = commit;
|
|
366
|
+
fs.writeFileSync(
|
|
367
|
+
path.join(tmpDir, '.code-walkthrough-result.json'),
|
|
368
|
+
JSON.stringify(walkthrough)
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
expect(checkWalkthrough(tmpDir).ok).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('does not execute shell syntax from a bypass-audit commit value', () => {
|
|
375
|
+
const targetCommit = createRepository(tmpDir, 'target bypass');
|
|
376
|
+
const markerFile = path.join(tmpDir, 'bypass-injection-marker');
|
|
377
|
+
const auditDir = path.join(tmpDir, '.xp-gate');
|
|
378
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
379
|
+
fs.writeFileSync(path.join(auditDir, 'bypass-audit.jsonl'), JSON.stringify({
|
|
380
|
+
type: 'precommit_bypass',
|
|
381
|
+
commit: `${targetCommit}; touch ${markerFile}; #`,
|
|
382
|
+
}));
|
|
383
|
+
|
|
384
|
+
const result = checkBypassAudit(tmpDir);
|
|
385
|
+
|
|
386
|
+
expect(result.bypassedCommits).toEqual([]);
|
|
387
|
+
expect(fs.existsSync(markerFile)).toBe(false);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
199
390
|
describe('Layer 1: Pre-transition gate check', () => {
|
|
200
391
|
it('warns when previous phase not completed', async () => {
|
|
201
392
|
// Set up: Phase 1 completed, then try Phase 3 in_progress (skipping Phase 2)
|
|
@@ -333,6 +524,375 @@ describe('phase-transition', () => {
|
|
|
333
524
|
fs.writeFileSync(path.join(outputsDir, 'test-alignment-report.json'), JSON.stringify(report, null, 2));
|
|
334
525
|
}
|
|
335
526
|
|
|
527
|
+
function writeRequirementsReview(dir, report) {
|
|
528
|
+
const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
|
|
529
|
+
fs.mkdirSync(outputsDir, { recursive: true });
|
|
530
|
+
fs.writeFileSync(path.join(outputsDir, 'requirements-reviewed.json'), JSON.stringify(report, null, 2));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function validRequirementsReview(dir, overrides = {}) {
|
|
534
|
+
const headCommit = fs.existsSync(path.join(dir, '.git'))
|
|
535
|
+
? git('rev-parse HEAD', dir)
|
|
536
|
+
: createRepository(dir, 'reviewed requirements');
|
|
537
|
+
const review = {
|
|
538
|
+
verdict: 'APPROVED',
|
|
539
|
+
requirements_statement: 'The release must preserve current quality gates.',
|
|
540
|
+
context_file_used: null,
|
|
541
|
+
timestamp: '2026-08-20T10:30:00.000Z',
|
|
542
|
+
head_commit: headCommit,
|
|
543
|
+
consensus_ratio: 0.9,
|
|
544
|
+
expert_verdicts: [
|
|
545
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
546
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
547
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
548
|
+
],
|
|
549
|
+
...overrides,
|
|
550
|
+
};
|
|
551
|
+
const contextContent = review.context_file_used
|
|
552
|
+
? fs.readFileSync(path.join(dir, review.context_file_used), 'utf8')
|
|
553
|
+
: '';
|
|
554
|
+
return {
|
|
555
|
+
...review,
|
|
556
|
+
requirements_hash: overrides.requirements_hash ?? crypto
|
|
557
|
+
.createHash('sha256')
|
|
558
|
+
.update(`${review.requirements_statement}${contextContent}${review.timestamp.slice(0, 10)}`, 'utf8')
|
|
559
|
+
.digest('hex'),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
it('BLOCKs schema-v2 requirements evidence without head_commit', () => {
|
|
564
|
+
createSprintState(tmpDir, 2);
|
|
565
|
+
writeRequirementsReview(tmpDir, {
|
|
566
|
+
verdict: 'APPROVED',
|
|
567
|
+
requirements_hash: 'requirements-v1',
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const result = validateEvidence(2, tmpDir);
|
|
571
|
+
|
|
572
|
+
expect(result.ok).toBe(false);
|
|
573
|
+
expect(result.errors.some(error => error.includes('head_commit'))).toBe(true);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('BLOCKs schema-v2 requirements evidence bound to a stale HEAD', () => {
|
|
577
|
+
const reviewedCommit = createRepository(tmpDir, 'reviewed requirements');
|
|
578
|
+
fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'requirements changed');
|
|
579
|
+
git('add file.txt', tmpDir);
|
|
580
|
+
git('commit --quiet -m "changed requirements"', tmpDir);
|
|
581
|
+
createSprintState(tmpDir, 2);
|
|
582
|
+
writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, { head_commit: reviewedCommit }));
|
|
583
|
+
|
|
584
|
+
const result = validateEvidence(2, tmpDir);
|
|
585
|
+
|
|
586
|
+
expect(result.ok).toBe(false);
|
|
587
|
+
expect(result.errors.some(error => error.includes('head_commit mismatch'))).toBe(true);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('accepts schema-v2 requirements evidence bound to the current HEAD', () => {
|
|
591
|
+
createSprintState(tmpDir, 2);
|
|
592
|
+
writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir));
|
|
593
|
+
|
|
594
|
+
const result = validateEvidence(2, tmpDir);
|
|
595
|
+
|
|
596
|
+
expect(result.ok).toBe(true);
|
|
597
|
+
expect(result.errors).toHaveLength(0);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it.each([
|
|
601
|
+
['requirements_statement', { requirements_statement: undefined }],
|
|
602
|
+
['timestamp', { timestamp: undefined }],
|
|
603
|
+
['consensus_ratio', { consensus_ratio: undefined }],
|
|
604
|
+
['expert_verdicts', { expert_verdicts: undefined }],
|
|
605
|
+
['head_commit', { head_commit: undefined }],
|
|
606
|
+
['requirements_hash', { requirements_hash: undefined }],
|
|
607
|
+
])('BLOCKs schema-v2 requirements evidence missing %s', (field, overrides) => {
|
|
608
|
+
createSprintState(tmpDir, 2);
|
|
609
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
610
|
+
delete evidence[field];
|
|
611
|
+
writeRequirementsReview(tmpDir, { ...evidence, ...overrides });
|
|
612
|
+
|
|
613
|
+
const result = validateEvidence(2, tmpDir);
|
|
614
|
+
|
|
615
|
+
expect(result.ok).toBe(false);
|
|
616
|
+
expect(result.errors.some(error => error.includes(field))).toBe(true);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it.each([
|
|
620
|
+
['non-hex hash', 'requirements-v1'],
|
|
621
|
+
['short hash', 'abc123'],
|
|
622
|
+
['valid-looking stale hash', '0'.repeat(64)],
|
|
623
|
+
])('BLOCKs schema-v2 requirements evidence with %s', (_label, requirementsHash) => {
|
|
624
|
+
createSprintState(tmpDir, 2);
|
|
625
|
+
writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, {
|
|
626
|
+
requirements_hash: requirementsHash,
|
|
627
|
+
}));
|
|
628
|
+
|
|
629
|
+
const result = validateEvidence(2, tmpDir);
|
|
630
|
+
|
|
631
|
+
expect(result.ok).toBe(false);
|
|
632
|
+
expect(result.errors.some(error => error.includes('requirements_hash'))).toBe(true);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('accepts an uppercase recomputable requirements hash', () => {
|
|
636
|
+
createSprintState(tmpDir, 2);
|
|
637
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
638
|
+
evidence.requirements_hash = evidence.requirements_hash.toUpperCase();
|
|
639
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
640
|
+
|
|
641
|
+
expect(validateEvidence(2, tmpDir).ok).toBe(true);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('BLOCKs when requirements_statement changes after hashing', () => {
|
|
645
|
+
createSprintState(tmpDir, 2);
|
|
646
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
647
|
+
evidence.requirements_statement = 'Changed after review.';
|
|
648
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
649
|
+
|
|
650
|
+
const result = validateEvidence(2, tmpDir);
|
|
651
|
+
|
|
652
|
+
expect(result.ok).toBe(false);
|
|
653
|
+
expect(result.errors.some(error => error.includes('requirements_hash mismatch'))).toBe(true);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('accepts a recomputable hash using a project-relative context file', () => {
|
|
657
|
+
createSprintState(tmpDir, 2);
|
|
658
|
+
fs.writeFileSync(path.join(tmpDir, 'CONTEXT.md'), 'bounded context\n', 'utf8');
|
|
659
|
+
writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, {
|
|
660
|
+
context_file_used: 'CONTEXT.md',
|
|
661
|
+
}));
|
|
662
|
+
|
|
663
|
+
expect(validateEvidence(2, tmpDir).ok).toBe(true);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('BLOCKs when context file content changes after hashing', () => {
|
|
667
|
+
createSprintState(tmpDir, 2);
|
|
668
|
+
fs.writeFileSync(path.join(tmpDir, 'CONTEXT.md'), 'original context\n', 'utf8');
|
|
669
|
+
const evidence = validRequirementsReview(tmpDir, { context_file_used: 'CONTEXT.md' });
|
|
670
|
+
fs.writeFileSync(path.join(tmpDir, 'CONTEXT.md'), 'changed context\n', 'utf8');
|
|
671
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
672
|
+
|
|
673
|
+
const result = validateEvidence(2, tmpDir);
|
|
674
|
+
|
|
675
|
+
expect(result.ok).toBe(false);
|
|
676
|
+
expect(result.errors.some(error => error.includes('requirements_hash mismatch'))).toBe(true);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it.each([
|
|
680
|
+
['missing path', 'missing/CONTEXT.md'],
|
|
681
|
+
['absolute path', '/tmp/CONTEXT.md'],
|
|
682
|
+
['traversal path', '../CONTEXT.md'],
|
|
683
|
+
])('BLOCKs schema-v2 requirements evidence with %s context', (_label, contextFileUsed) => {
|
|
684
|
+
createSprintState(tmpDir, 2);
|
|
685
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
686
|
+
evidence.context_file_used = contextFileUsed;
|
|
687
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
688
|
+
|
|
689
|
+
const result = validateEvidence(2, tmpDir);
|
|
690
|
+
|
|
691
|
+
expect(result.ok).toBe(false);
|
|
692
|
+
expect(result.errors.some(error => error.includes('context_file_used'))).toBe(true);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('BLOCKs a context symlink that resolves outside the project root', () => {
|
|
696
|
+
createSprintState(tmpDir, 2);
|
|
697
|
+
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'requirements-context-'));
|
|
698
|
+
const outsideFile = path.join(outsideDir, 'CONTEXT.md');
|
|
699
|
+
fs.writeFileSync(outsideFile, 'outside context\n', 'utf8');
|
|
700
|
+
fs.symlinkSync(outsideFile, path.join(tmpDir, 'CONTEXT.md'));
|
|
701
|
+
const evidence = validRequirementsReview(tmpDir, { context_file_used: 'CONTEXT.md' });
|
|
702
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
703
|
+
|
|
704
|
+
const result = validateEvidence(2, tmpDir);
|
|
705
|
+
|
|
706
|
+
expect(result.ok).toBe(false);
|
|
707
|
+
expect(result.errors.some(error => error.includes('context_file_used'))).toBe(true);
|
|
708
|
+
fs.rmSync(outsideDir, { recursive: true, force: true });
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it.each([
|
|
712
|
+
['invalid timestamp', 'not-a-timestamp'],
|
|
713
|
+
['invalid calendar date', '2026-02-30T10:30:00.000Z'],
|
|
714
|
+
])('BLOCKs schema-v2 requirements evidence with %s', (_label, timestamp) => {
|
|
715
|
+
createSprintState(tmpDir, 2);
|
|
716
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
717
|
+
evidence.timestamp = timestamp;
|
|
718
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
719
|
+
|
|
720
|
+
const result = validateEvidence(2, tmpDir);
|
|
721
|
+
|
|
722
|
+
expect(result.ok).toBe(false);
|
|
723
|
+
expect(result.errors.some(error => error.includes('timestamp'))).toBe(true);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it.each([
|
|
727
|
+
['missing expert_verdicts', { expert_verdicts: undefined }, 'expert_verdicts'],
|
|
728
|
+
['too few expert_verdicts', { expert_verdicts: [
|
|
729
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
730
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
731
|
+
] }, 'exactly 3'],
|
|
732
|
+
['too many expert_verdicts', { expert_verdicts: [
|
|
733
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
734
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
735
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
736
|
+
{ role: 'extra', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-d' },
|
|
737
|
+
] }, 'exactly 3'],
|
|
738
|
+
['duplicate role', { expert_verdicts: [
|
|
739
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
740
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
741
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
742
|
+
] }, 'roles'],
|
|
743
|
+
['missing role', { expert_verdicts: [
|
|
744
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
745
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
746
|
+
{ verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
747
|
+
] }, 'roles'],
|
|
748
|
+
['non-approved expert verdict', { expert_verdicts: [
|
|
749
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
750
|
+
{ role: 'technical', verdict: 'REQUEST_CHANGES', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
751
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
752
|
+
] }, 'APPROVED'],
|
|
753
|
+
['wrong result_type', { expert_verdicts: [
|
|
754
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'summary', requested_model: 'model-a' },
|
|
755
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
756
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
757
|
+
] }, 'result_type'],
|
|
758
|
+
['blank requested_model', { expert_verdicts: [
|
|
759
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: ' ' },
|
|
760
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
761
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
762
|
+
] }, 'requested_model'],
|
|
763
|
+
['duplicate trimmed requested_model', { expert_verdicts: [
|
|
764
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
|
|
765
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: ' model-a ' },
|
|
766
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
767
|
+
] }, 'distinct'],
|
|
768
|
+
])('BLOCKs schema-v2 requirements evidence with %s', (_label, overrides, expectedError) => {
|
|
769
|
+
createSprintState(tmpDir, 2);
|
|
770
|
+
writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, overrides));
|
|
771
|
+
|
|
772
|
+
const result = validateEvidence(2, tmpDir);
|
|
773
|
+
|
|
774
|
+
expect(result.ok).toBe(false);
|
|
775
|
+
expect(result.errors.some(error => error.includes(expectedError))).toBe(true);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it.each([
|
|
779
|
+
['error true', { error: true }],
|
|
780
|
+
['error string', { error: 'provider failed' }],
|
|
781
|
+
['error false-shaped string', { error: 'false' }],
|
|
782
|
+
['fallback true', { fallback: true }],
|
|
783
|
+
['fallback object', { fallback: { provider: 'local' } }],
|
|
784
|
+
['fallback false-shaped number', { fallback: 0 }],
|
|
785
|
+
])('BLOCKs schema-v2 requirements evidence with an expert %s marker', (_label, marker) => {
|
|
786
|
+
createSprintState(tmpDir, 2);
|
|
787
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
788
|
+
Object.assign(evidence.expert_verdicts[0], marker);
|
|
789
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
790
|
+
|
|
791
|
+
const result = validateEvidence(2, tmpDir);
|
|
792
|
+
|
|
793
|
+
expect(result.ok).toBe(false);
|
|
794
|
+
expect(result.errors.some(error => error.includes('error or fallback marker'))).toBe(true);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('accepts schema-v2 expert records with omitted or explicitly clear markers', () => {
|
|
798
|
+
createSprintState(tmpDir, 2);
|
|
799
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
800
|
+
evidence.expert_verdicts[0].error = false;
|
|
801
|
+
evidence.expert_verdicts[0].fallback = null;
|
|
802
|
+
evidence.expert_verdicts[1].error = null;
|
|
803
|
+
evidence.expert_verdicts[1].fallback = false;
|
|
804
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
805
|
+
|
|
806
|
+
expect(validateEvidence(2, tmpDir).ok).toBe(true);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('does not include arbitrary expert payloads in validation errors', () => {
|
|
810
|
+
createSprintState(tmpDir, 2);
|
|
811
|
+
writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, {
|
|
812
|
+
verdict: 'REQUEST_CHANGES',
|
|
813
|
+
expert_verdicts: [{ private_payload: 'do-not-leak' }],
|
|
814
|
+
}));
|
|
815
|
+
|
|
816
|
+
const result = validateEvidence(2, tmpDir);
|
|
817
|
+
|
|
818
|
+
expect(result.ok).toBe(false);
|
|
819
|
+
expect(result.errors.join('\n')).not.toContain('do-not-leak');
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it.each([
|
|
823
|
+
['missing', undefined],
|
|
824
|
+
['non-number', '0.95'],
|
|
825
|
+
['below threshold', 0.89],
|
|
826
|
+
['above one', 1.01],
|
|
827
|
+
['non-finite', Number.POSITIVE_INFINITY],
|
|
828
|
+
])('BLOCKs schema-v2 requirements evidence with %s consensus_ratio', (_label, consensusRatio) => {
|
|
829
|
+
createSprintState(tmpDir, 2);
|
|
830
|
+
const evidence = validRequirementsReview(tmpDir, { consensus_ratio: consensusRatio });
|
|
831
|
+
if (consensusRatio === undefined) delete evidence.consensus_ratio;
|
|
832
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
833
|
+
|
|
834
|
+
const result = validateEvidence(2, tmpDir);
|
|
835
|
+
|
|
836
|
+
expect(result.ok).toBe(false);
|
|
837
|
+
expect(result.errors.some(error => error.includes('consensus_ratio'))).toBe(true);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('BLOCKs schema-v2 requirements evidence when Git HEAD cannot be resolved', () => {
|
|
841
|
+
createSprintState(tmpDir, 2);
|
|
842
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
843
|
+
fs.rmSync(path.join(tmpDir, '.git'), { recursive: true, force: true });
|
|
844
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
845
|
+
|
|
846
|
+
const result = validateEvidence(2, tmpDir);
|
|
847
|
+
|
|
848
|
+
expect(result.ok).toBe(false);
|
|
849
|
+
expect(result.errors.some(error => error.includes('resolve current Git HEAD'))).toBe(true);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it.each([
|
|
853
|
+
['invalid expert evidence', { expert_verdicts: [] }, 'expert_verdicts'],
|
|
854
|
+
['expert failure marker', { expert_verdicts: [
|
|
855
|
+
{ role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a', error: true },
|
|
856
|
+
{ role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
|
|
857
|
+
{ role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
|
|
858
|
+
] }, 'error or fallback marker'],
|
|
859
|
+
['unresolved HEAD', null, 'resolve current Git HEAD'],
|
|
860
|
+
['stale HEAD', { head_commit: 'deadbeef00000000000000000000000000000000' }, 'head_commit mismatch'],
|
|
861
|
+
['stale requirements hash', { requirements_hash: '0'.repeat(64) }, 'requirements_hash'],
|
|
862
|
+
['invalid timestamp', { timestamp: 'not-a-timestamp' }, 'timestamp'],
|
|
863
|
+
['unsafe context path', { context_file_used: '../CONTEXT.md' }, 'context_file_used'],
|
|
864
|
+
])('WARNs but accepts legacy requirements evidence with %s', (_label, overrides, expectedWarning) => {
|
|
865
|
+
const evidence = validRequirementsReview(tmpDir);
|
|
866
|
+
if (overrides) Object.assign(evidence, overrides);
|
|
867
|
+
if (overrides === null) fs.rmSync(path.join(tmpDir, '.git'), { recursive: true, force: true });
|
|
868
|
+
createSprintState(tmpDir);
|
|
869
|
+
writeRequirementsReview(tmpDir, evidence);
|
|
870
|
+
|
|
871
|
+
const result = validateEvidence(2, tmpDir);
|
|
872
|
+
|
|
873
|
+
expect(result.ok).toBe(true);
|
|
874
|
+
expect(result.warnings.some(warning => warning.includes(expectedWarning))).toBe(true);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it.each([
|
|
878
|
+
['missing', undefined],
|
|
879
|
+
['stale', 'deadbeef00000000000000000000000000000000'],
|
|
880
|
+
])('WARNs but accepts legacy requirements evidence with %s head_commit', (_label, headCommit) => {
|
|
881
|
+
createRepository(tmpDir, 'reviewed requirements');
|
|
882
|
+
createSprintState(tmpDir);
|
|
883
|
+
const report = {
|
|
884
|
+
verdict: 'APPROVED',
|
|
885
|
+
requirements_hash: 'requirements-v1',
|
|
886
|
+
};
|
|
887
|
+
if (headCommit !== undefined) report.head_commit = headCommit;
|
|
888
|
+
writeRequirementsReview(tmpDir, report);
|
|
889
|
+
|
|
890
|
+
const result = validateEvidence(2, tmpDir);
|
|
891
|
+
|
|
892
|
+
expect(result.ok).toBe(true);
|
|
893
|
+
expect(result.warnings.some(warning => warning.includes('head_commit'))).toBe(true);
|
|
894
|
+
});
|
|
895
|
+
|
|
336
896
|
it('(a) BLOCKs when evidence file is missing for new sprint (evidence_schema_version >= 2)', () => {
|
|
337
897
|
createSprintState(tmpDir, 2);
|
|
338
898
|
const result = validateEvidence(4, tmpDir);
|
|
@@ -355,6 +915,7 @@ describe('phase-transition', () => {
|
|
|
355
915
|
});
|
|
356
916
|
|
|
357
917
|
it('(c) BLOCKs when head_commit is stale (does not match current HEAD)', () => {
|
|
918
|
+
createRepository(tmpDir, 'aligned tests');
|
|
358
919
|
createSprintState(tmpDir, 2);
|
|
359
920
|
writeAlignmentReport(tmpDir, {
|
|
360
921
|
alignment_status: 'PASS',
|
|
@@ -363,12 +924,38 @@ describe('phase-transition', () => {
|
|
|
363
924
|
timestamp: new Date().toISOString(),
|
|
364
925
|
});
|
|
365
926
|
const result = validateEvidence(4, tmpDir);
|
|
366
|
-
// In tmpDir (no git repo), current HEAD is 'unknown'.
|
|
367
|
-
// A non-'unknown' head_commit that doesn't match 'unknown' should BLOCK.
|
|
368
927
|
expect(result.ok).toBe(false);
|
|
369
928
|
expect(result.errors.some(e => e.includes('head_commit'))).toBe(true);
|
|
370
929
|
});
|
|
371
930
|
|
|
931
|
+
it('BLOCKs schema-v2 phase 4 evidence when Git HEAD cannot be resolved', () => {
|
|
932
|
+
createSprintState(tmpDir, 2);
|
|
933
|
+
writeAlignmentReport(tmpDir, {
|
|
934
|
+
alignment_status: 'PASS',
|
|
935
|
+
head_commit: 'unknown',
|
|
936
|
+
spec_hash: null,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const result = validateEvidence(4, tmpDir);
|
|
940
|
+
|
|
941
|
+
expect(result.ok).toBe(false);
|
|
942
|
+
expect(result.errors.some(error => error.includes('resolve current Git HEAD'))).toBe(true);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('WARNs but accepts legacy phase 4 evidence when Git HEAD cannot be resolved', () => {
|
|
946
|
+
createSprintState(tmpDir);
|
|
947
|
+
writeAlignmentReport(tmpDir, {
|
|
948
|
+
alignment_status: 'PASS',
|
|
949
|
+
head_commit: 'unknown',
|
|
950
|
+
spec_hash: null,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
const result = validateEvidence(4, tmpDir);
|
|
954
|
+
|
|
955
|
+
expect(result.ok).toBe(true);
|
|
956
|
+
expect(result.warnings.some(warning => warning.includes('resolve current Git HEAD'))).toBe(true);
|
|
957
|
+
});
|
|
958
|
+
|
|
372
959
|
it('(d) BLOCKs when evidence file is malformed JSON (treated as missing)', () => {
|
|
373
960
|
createSprintState(tmpDir, 2);
|
|
374
961
|
const outputsDir = path.join(tmpDir, '.sprint-state', 'phase-outputs');
|
|
@@ -404,6 +991,7 @@ describe('phase-transition', () => {
|
|
|
404
991
|
expect(lastEntry.event).toBe('evidence_skipped');
|
|
405
992
|
expect(lastEntry.phase).toBe(4);
|
|
406
993
|
expect(lastEntry.reason).toBe('Emergency hotfix');
|
|
994
|
+
expect(lastEntry.commit_hash).toBe('unknown');
|
|
407
995
|
});
|
|
408
996
|
|
|
409
997
|
it('(g) rejects --skip-evidence without --reason', async () => {
|
|
@@ -424,11 +1012,12 @@ describe('phase-transition', () => {
|
|
|
424
1012
|
}
|
|
425
1013
|
});
|
|
426
1014
|
|
|
427
|
-
it('accepts valid evidence with head_commit matching current HEAD
|
|
1015
|
+
it('accepts valid phase 4 evidence with head_commit matching current HEAD', () => {
|
|
1016
|
+
const currentHead = createRepository(tmpDir, 'aligned tests');
|
|
428
1017
|
createSprintState(tmpDir, 2);
|
|
429
1018
|
writeAlignmentReport(tmpDir, {
|
|
430
1019
|
alignment_status: 'PASS',
|
|
431
|
-
head_commit:
|
|
1020
|
+
head_commit: currentHead,
|
|
432
1021
|
spec_hash: null,
|
|
433
1022
|
timestamp: new Date().toISOString(),
|
|
434
1023
|
});
|
|
@@ -438,6 +1027,7 @@ describe('phase-transition', () => {
|
|
|
438
1027
|
});
|
|
439
1028
|
|
|
440
1029
|
it('validates spec_hash when specification.yaml exists', () => {
|
|
1030
|
+
const currentHead = createRepository(tmpDir, 'aligned specification');
|
|
441
1031
|
createSprintState(tmpDir, 2);
|
|
442
1032
|
// Create a specification.yaml
|
|
443
1033
|
const specContent = 'requirements:\n - id: REQ-001\n';
|
|
@@ -449,7 +1039,7 @@ describe('phase-transition', () => {
|
|
|
449
1039
|
|
|
450
1040
|
writeAlignmentReport(tmpDir, {
|
|
451
1041
|
alignment_status: 'PASS',
|
|
452
|
-
head_commit:
|
|
1042
|
+
head_commit: currentHead,
|
|
453
1043
|
spec_hash: expectedHash,
|
|
454
1044
|
timestamp: new Date().toISOString(),
|
|
455
1045
|
});
|
|
@@ -458,11 +1048,12 @@ describe('phase-transition', () => {
|
|
|
458
1048
|
});
|
|
459
1049
|
|
|
460
1050
|
it('BLOCKs when spec_hash does not match specification.yaml', () => {
|
|
1051
|
+
const currentHead = createRepository(tmpDir, 'stale specification');
|
|
461
1052
|
createSprintState(tmpDir, 2);
|
|
462
1053
|
fs.writeFileSync(path.join(tmpDir, 'specification.yaml'), 'requirements:\n - id: REQ-001\n');
|
|
463
1054
|
writeAlignmentReport(tmpDir, {
|
|
464
1055
|
alignment_status: 'PASS',
|
|
465
|
-
head_commit:
|
|
1056
|
+
head_commit: currentHead,
|
|
466
1057
|
spec_hash: 'wronghash000000000000000000000000000000000000000000000000000000',
|
|
467
1058
|
timestamp: new Date().toISOString(),
|
|
468
1059
|
});
|
|
@@ -498,10 +1089,11 @@ describe('phase-transition', () => {
|
|
|
498
1089
|
});
|
|
499
1090
|
|
|
500
1091
|
it('handlePhaseTransition allows phase 4 completed with valid evidence', async () => {
|
|
1092
|
+
const currentHead = createRepository(tmpDir, 'aligned tests');
|
|
501
1093
|
createSprintState(tmpDir, 2);
|
|
502
1094
|
writeAlignmentReport(tmpDir, {
|
|
503
1095
|
alignment_status: 'PASS',
|
|
504
|
-
head_commit:
|
|
1096
|
+
head_commit: currentHead,
|
|
505
1097
|
spec_hash: null,
|
|
506
1098
|
timestamp: new Date().toISOString(),
|
|
507
1099
|
});
|