@xulthekl/team-flow 0.30.0 → 0.32.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/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/marketplace.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +5 -4
- package/CHANGELOG.md +85 -0
- package/GEMINI.md +1 -1
- package/HANDOFF.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +3 -3
- package/agents/build-executor.md +1 -0
- package/agents/contract-builder.md +6 -1
- package/docs/README_en.md +1 -1
- package/docs/solutions/INDEX.md +2 -0
- package/docs/solutions/cross-phase/2026-08-01-no-summary.md +17 -0
- package/docs/solutions/cross-phase/2026-08-03-no-summary.md +17 -0
- package/gemini-extension.json +2 -2
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +6 -2
- package/plugin.json +2 -2
- package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
- package/scripts/guard/checks/test-matrix-complete.mjs +91 -0
- package/scripts/guard/checks/test-matrix-ready.mjs +60 -0
- package/scripts/guard/checks/tests-passing.mjs +112 -24
- package/scripts/guard/guard.mjs +10 -2
- package/scripts/lib/cmd-doctor.mjs +43 -1
- package/scripts/lib/cmd-execution.mjs +24 -1
- package/scripts/lib/cmd-state.mjs +33 -5
- package/scripts/lib/cmd-validate.mjs +42 -0
- package/scripts/lib/execution-plan.mjs +23 -0
- package/scripts/lib/hash.mjs +11 -0
- package/scripts/lib/state-loader.mjs +26 -1
- package/scripts/lib/test-matrix-export.mjs +231 -0
- package/scripts/lib/test-merge.mjs +540 -0
- package/scripts/lib/test-record.mjs +212 -0
- package/scripts/team-flow.mjs +9 -0
- package/skills/build-executor/implementer-prompt.md +38 -3
- package/skills/code-reviewer/SKILL.md +28 -1
- package/skills/code-reviewer/code-reviewer-prompt.md +10 -0
- package/skills/contract-builder/SKILL.md +75 -0
- package/skills/release-archivist/SKILL.md +50 -10
- package/skills/session-handoff/SKILL.md +1 -0
- package/skills/spec-writer/SKILL.md +3 -1
- package/skills/test-strategy/SKILL.md +70 -0
- package/skills/test-strategy/references/adversarial-patterns.md +0 -0
- package/skills/test-strategy/references/complexity-grading.md +137 -0
- package/skills/test-strategy/references/design-methods-detail.md +183 -0
- package/skills/workflow-orchestrator/references/s1-path-router.md +4 -0
- package/skills/workflow-start/SKILL.md +5 -3
- package/skills/workflow-start/references/routing-rules.md +21 -0
- package/tests/e2e.test.mjs +0 -266
- package/tests/lib/cmd-audit.test.mjs +0 -202
- package/tests/lib/cmd-checkpoint.test.mjs +0 -98
- package/tests/lib/cmd-config.test.mjs +0 -101
- package/tests/lib/cmd-doctor.test.mjs +0 -450
- package/tests/lib/cmd-execution.test.mjs +0 -647
- package/tests/lib/cmd-handoff.test.mjs +0 -137
- package/tests/lib/cmd-inject.test.mjs +0 -218
- package/tests/lib/cmd-install-workbuddy.test.mjs +0 -113
- package/tests/lib/cmd-install-zcode.test.mjs +0 -42
- package/tests/lib/cmd-isolate.test.mjs +0 -68
- package/tests/lib/cmd-list.test.mjs +0 -127
- package/tests/lib/cmd-runtime.test.mjs +0 -83
- package/tests/lib/cmd-state-missing.test.mjs +0 -65
- package/tests/lib/cmd-state.test.mjs +0 -365
- package/tests/lib/cmd-sync-paths.test.mjs +0 -84
- package/tests/lib/cmd-validate-paths.test.mjs +0 -90
- package/tests/lib/config-loader.test.mjs +0 -175
- package/tests/lib/ensure-branch.test.mjs +0 -110
- package/tests/lib/execution-control-plane.test.mjs +0 -168
- package/tests/lib/execution-plan.test.mjs +0 -474
- package/tests/lib/guard-compound-captured.test.mjs +0 -92
- package/tests/lib/guard-specs-merged.test.mjs +0 -110
- package/tests/lib/guard-tests-passing.test.mjs +0 -114
- package/tests/lib/guard-transitions.test.mjs +0 -262
- package/tests/lib/guard.test.mjs +0 -613
- package/tests/lib/hash.test.mjs +0 -138
- package/tests/lib/infer-workflow.test.mjs +0 -161
- package/tests/lib/install-git-hooks.test.mjs +0 -20
- package/tests/lib/marketplace-release-docs.test.mjs +0 -31
- package/tests/lib/minimality-discipline.test.mjs +0 -37
- package/tests/lib/model-profiles-docs.test.mjs +0 -33
- package/tests/lib/node20-compatibility.test.mjs +0 -73
- package/tests/lib/node20-test-entry.test.mjs +0 -17
- package/tests/lib/platform-runtime-distribution.test.mjs +0 -157
- package/tests/lib/raw-mode-smoke.test.mjs +0 -42
- package/tests/lib/sdd-overlay.test.mjs +0 -118
- package/tests/lib/solutions-capture.test.mjs +0 -108
- package/tests/lib/solutions-index-gen.test.mjs +0 -147
- package/tests/lib/solutions-inject.test.mjs +0 -115
- package/tests/lib/solutions-promote.test.mjs +0 -200
- package/tests/lib/spec-paths.test.mjs +0 -80
- package/tests/lib/state-loader.test.mjs +0 -251
- package/tests/lib/token-baseline.test.mjs +0 -72
- package/tests/lib/token-rules.test.mjs +0 -94
- package/tests/lib/verify-marketplace-release.test.mjs +0 -128
- package/tests/tsconfig.json +0 -12
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// scripts/guard/checks/test-matrix-ready.mjs — test preparedness gate before executing
|
|
2
|
+
//
|
|
3
|
+
// v0.13 §49(设计增强方案 v0.13,C1-domain-policy 事件修复,RC-3 声明驱动型绕过):
|
|
4
|
+
// full workflow 的 approved-for-build→executing 新增本维度——进入执行前强制
|
|
5
|
+
// "带着矩阵开工",缺陷在开工前暴露,而不是等 45 个文件写完才在 closing 发现。
|
|
6
|
+
// 与 test-matrix-complete(出口)共用 legacy/skip 豁免语义(test-gate-exemptions.mjs)。
|
|
7
|
+
import { readState } from '../../lib/state-loader.mjs';
|
|
8
|
+
import {
|
|
9
|
+
isLegacyChange,
|
|
10
|
+
skipMissingReason,
|
|
11
|
+
SKIP_REASON_HINT,
|
|
12
|
+
contractDeclaresTestMatrix,
|
|
13
|
+
readTestMatrixFile,
|
|
14
|
+
} from './test-gate-exemptions.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns { pass, failures[], reason? }.
|
|
18
|
+
*/
|
|
19
|
+
export function checkTestMatrixReady(changeDir) {
|
|
20
|
+
const state = readState(changeDir);
|
|
21
|
+
|
|
22
|
+
// Exemption 1:legacy change(v0.32.0 之前初始化,无 schema_version 打戳)。
|
|
23
|
+
if (isLegacyChange(state)) {
|
|
24
|
+
return { pass: true, failures: [], reason: 'legacy change — initialized before v0.32.0' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Exemption 2:显式 skip(必须附理由)。
|
|
28
|
+
if (state.test_matrix_skipped === 'true') {
|
|
29
|
+
if (skipMissingReason(state)) {
|
|
30
|
+
return { pass: false, failures: [SKIP_REASON_HINT] };
|
|
31
|
+
}
|
|
32
|
+
return { pass: true, failures: [], reason: `explicitly skipped: ${state.test_matrix_skip_reason}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 正常路径:契约声明 ## Test Matrix 且 test-matrix.md 存在非空。
|
|
36
|
+
const contract = contractDeclaresTestMatrix(changeDir);
|
|
37
|
+
const matrix = readTestMatrixFile(changeDir);
|
|
38
|
+
if (contract.declares && matrix.exists && !matrix.empty) {
|
|
39
|
+
return { pass: true, failures: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const failures = [];
|
|
43
|
+
if (!contract.exists) {
|
|
44
|
+
failures.push('execution-contract.md is missing — run contract-builder first');
|
|
45
|
+
} else if (!contract.declares) {
|
|
46
|
+
failures.push('contract does not declare ## Test Matrix — return to bridging so contract-builder generates test-matrix.md');
|
|
47
|
+
}
|
|
48
|
+
if (contract.declares) {
|
|
49
|
+
if (!matrix.exists) {
|
|
50
|
+
failures.push('test-matrix.md is missing — generate it during bridging (contract-builder Test Matrix Generation), then run tf state rebuild');
|
|
51
|
+
} else if (matrix.empty) {
|
|
52
|
+
failures.push('test-matrix.md is empty — must contain at least a Summary section and Cases table');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
failures.push(
|
|
56
|
+
"if this change legitimately needs no test matrix, skip explicitly: "
|
|
57
|
+
+ "tf state set <dir> test_matrix_skipped true + test_matrix_skip_reason '<reason>'"
|
|
58
|
+
);
|
|
59
|
+
return { pass: false, failures };
|
|
60
|
+
}
|
|
@@ -1,31 +1,119 @@
|
|
|
1
|
-
// scripts/guard/checks/tests-passing.mjs — verify
|
|
1
|
+
// scripts/guard/checks/tests-passing.mjs — verify test execution evidence is recorded
|
|
2
|
+
//
|
|
3
|
+
// v0.13 §50 重写(BUG-A 政策修正,C1-domain-policy 事件修复,RC-2 门禁锚错证据):
|
|
4
|
+
// - 非存量 change 必须提供 `tf test record` 程序化写入的结构化证据
|
|
5
|
+
// (recorded-by=tf-test-record + 证据文件存在 + failed=0 + total>0);
|
|
6
|
+
// - 废除 dp_6_result 作为等价证据(dp_6 仍是决策点记录,但不再是门禁信号);
|
|
7
|
+
// - 关闭空真:total=0 一律拒绝(除非显式 skip/legacy);
|
|
8
|
+
// - 存量 change(无 schema_version 打戳)保留 v0.31 行为——BUG-A 通道仅对存量开放。
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
2
11
|
import { readState } from '../../lib/state-loader.mjs';
|
|
12
|
+
import { isLegacyChange, skipMissingReason, SKIP_REASON_HINT } from './test-gate-exemptions.mjs';
|
|
3
13
|
|
|
4
14
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
* 解析结构化 test_result:
|
|
16
|
+
* "pass: total=42 passed=42 failed=0 skipped=2 runner=maven-surefire recorded-by=tf-test-record ts=..."
|
|
17
|
+
* 返回 { verdict, total, passed, failed, skipped, runner, recordedBy, ts } 或 null。
|
|
18
|
+
*/
|
|
19
|
+
export function parseStructuredTestResult(raw) {
|
|
20
|
+
if (typeof raw !== 'string') return null;
|
|
21
|
+
const text = raw.trim().replace(/^["']|["']$/g, '');
|
|
22
|
+
const m = text.match(/^(pass|fail):\s*(.*)$/i);
|
|
23
|
+
if (!m) return null;
|
|
24
|
+
const fields = {};
|
|
25
|
+
for (const kv of m[2].matchAll(/([A-Za-z-]+)=(\S+)/g)) fields[kv[1]] = kv[2];
|
|
26
|
+
const toInt = (key) => (fields[key] !== undefined && /^\d+$/.test(fields[key]) ? parseInt(fields[key], 10) : null);
|
|
27
|
+
return {
|
|
28
|
+
verdict: m[1].toLowerCase(),
|
|
29
|
+
total: toInt('total'),
|
|
30
|
+
passed: toInt('passed'),
|
|
31
|
+
failed: toInt('failed'),
|
|
32
|
+
skipped: toInt('skipped'),
|
|
33
|
+
runner: fields.runner ?? null,
|
|
34
|
+
recordedBy: fields['recorded-by'] ?? null,
|
|
35
|
+
ts: fields.ts ?? null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SKIP_OR_RECORD_HINT =
|
|
40
|
+
"Run the project test suite, then record it: tf test record <dir> --from <runner-output-file>. "
|
|
41
|
+
+ "If this change legitimately has no automated tests, skip explicitly: "
|
|
42
|
+
+ "test_matrix_skipped=true + test_matrix_skip_reason='<reason>'";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns { pass, failures[], reason? }.
|
|
13
46
|
*/
|
|
14
47
|
export function checkTestsPassing(changeDir) {
|
|
15
48
|
const state = readState(changeDir);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
49
|
+
|
|
50
|
+
// Legacy(无 schema_version):保留 v0.31 行为(BUG-A 通道仅对存量开放)。
|
|
51
|
+
if (isLegacyChange(state)) {
|
|
52
|
+
const markers = [state.test_result, state.dp_6_result];
|
|
53
|
+
const passed = markers.some((v) => {
|
|
54
|
+
if (typeof v !== 'string') return false;
|
|
55
|
+
const normalized = v.trim().replace(/^["']|["']$/g, '');
|
|
56
|
+
return normalized.toLowerCase().startsWith('pass');
|
|
57
|
+
});
|
|
58
|
+
if (passed) {
|
|
59
|
+
return { pass: true, failures: [] };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
pass: false,
|
|
63
|
+
failures: [
|
|
64
|
+
`verification outcome missing (legacy): test_result is '${state.test_result || 'null'}' and dp_6_result is '${state.dp_6_result || 'null'}' — expected a result starting with 'pass'. Run release-archivist verification (DP-6) first.`,
|
|
65
|
+
],
|
|
66
|
+
};
|
|
24
67
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
68
|
+
|
|
69
|
+
// 显式 skip(必须附理由)。
|
|
70
|
+
if (state.test_matrix_skipped === 'true') {
|
|
71
|
+
if (skipMissingReason(state)) {
|
|
72
|
+
return { pass: false, failures: [SKIP_REASON_HINT] };
|
|
73
|
+
}
|
|
74
|
+
return { pass: true, failures: [], reason: `explicitly skipped: ${state.test_matrix_skip_reason}` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 非存量:要求 tf test record 写入的结构化证据。
|
|
78
|
+
const parsed = parseStructuredTestResult(state.test_result);
|
|
79
|
+
if (!parsed) {
|
|
80
|
+
return {
|
|
81
|
+
pass: false,
|
|
82
|
+
failures: [`no programmatic test evidence recorded — test_result is '${state.test_result || 'null'}'. ${SKIP_OR_RECORD_HINT}`],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (parsed.recordedBy !== 'tf-test-record') {
|
|
86
|
+
return {
|
|
87
|
+
pass: false,
|
|
88
|
+
failures: [
|
|
89
|
+
"test_result must be recorded by 'tf test record' (programmatic evidence); manually written values are not accepted (v0.13 §50)",
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// 证据文件必须存在(纵深防御:记录必须可追溯到落盘的 runner 输出)。
|
|
94
|
+
if (!state.test_evidence_path || !fs.existsSync(path.join(changeDir, state.test_evidence_path))) {
|
|
95
|
+
return {
|
|
96
|
+
pass: false,
|
|
97
|
+
failures: [
|
|
98
|
+
'test evidence file missing — test_result must trace to a saved runner output recorded by tf test record (.superpowers/test-evidence/)',
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (parsed.verdict !== 'pass' || (parsed.failed ?? 0) !== 0) {
|
|
103
|
+
return {
|
|
104
|
+
pass: false,
|
|
105
|
+
failures: [
|
|
106
|
+
`test suite reported failures (failed=${parsed.failed ?? '?'}) — fix the failures, re-run the suite, and record again via tf test record`,
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!Number.isInteger(parsed.total) || parsed.total <= 0) {
|
|
111
|
+
return {
|
|
112
|
+
pass: false,
|
|
113
|
+
failures: [
|
|
114
|
+
'test_result records total=0 — an empty suite is not evidence; add tests or skip explicitly (test_matrix_skipped=true + reason)',
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { pass: true, failures: [] };
|
|
119
|
+
}
|
package/scripts/guard/guard.mjs
CHANGED
|
@@ -14,6 +14,8 @@ import { checkDp3Approved } from './checks/dp3-approved.mjs';
|
|
|
14
14
|
import { checkExecutionPlanReady } from './checks/execution-plan-ready.mjs';
|
|
15
15
|
import { checkExecutionReviewsPassed } from './checks/execution-reviews-passed.mjs';
|
|
16
16
|
import { checkCompoundCaptured } from './checks/compound-captured.mjs';
|
|
17
|
+
import { checkTestMatrixComplete } from './checks/test-matrix-complete.mjs';
|
|
18
|
+
import { checkTestMatrixReady } from './checks/test-matrix-ready.mjs';
|
|
17
19
|
|
|
18
20
|
// Transition matrix: <from>:<to> → required check dimensions
|
|
19
21
|
const TRANSITION_CHECKS = {
|
|
@@ -21,8 +23,10 @@ const TRANSITION_CHECKS = {
|
|
|
21
23
|
'exploring:specifying': ['artifacts-exist', 'arch-design'],
|
|
22
24
|
'specifying:bridging': ['artifacts-exist', 'schema-valid'],
|
|
23
25
|
'bridging:approved-for-build': ['artifacts-exist', 'schema-valid', 'contract-fresh', 'dp-gate-passed'],
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
// v0.13 §49:test-matrix-ready 门禁前移——full 模式进入 executing 前强制测试准备度
|
|
27
|
+
//(矩阵存在非空 OR 显式 skip 附理由;legacy 豁免)。hotfix/tweak 沿用 §45.3 豁免,不挂。
|
|
28
|
+
'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed', 'execution-plan-ready', 'test-matrix-ready'],
|
|
29
|
+
'executing:closing': ['tasks-complete', 'tests-passing', 'specs-merged', 'execution-plan-ready', 'execution-reviews-passed', 'compound-captured', 'test-matrix-complete'],
|
|
26
30
|
|
|
27
31
|
// Debugging side-path
|
|
28
32
|
'executing:debugging': [],
|
|
@@ -55,6 +59,8 @@ const WORKFLOW_TRANSITION_CHECKS = {
|
|
|
55
59
|
'exploring:bridging': [],
|
|
56
60
|
'bridging:approved-for-build': ['contract-current', 'dp3-approved'],
|
|
57
61
|
'approved-for-build:executing': ['contract-current', 'dp3-approved', 'execution-plan-ready'],
|
|
62
|
+
// hotfix exemption: no test-matrix-complete (v0.12 §45.3)
|
|
63
|
+
'executing:closing': ['tasks-complete', 'tests-passing', 'specs-merged', 'execution-plan-ready', 'execution-reviews-passed', 'compound-captured'],
|
|
58
64
|
},
|
|
59
65
|
tweak: {
|
|
60
66
|
'exploring:approved-for-build': [],
|
|
@@ -167,6 +173,8 @@ async function main() {
|
|
|
167
173
|
'execution-reviews-passed': (dir) => checkExecutionReviewsPassed(dir),
|
|
168
174
|
'arch-design': (dir) => checkArchDesign(dir),
|
|
169
175
|
'compound-captured': (dir) => checkCompoundCaptured(dir),
|
|
176
|
+
'test-matrix-complete': (dir) => checkTestMatrixComplete(dir),
|
|
177
|
+
'test-matrix-ready': (dir) => checkTestMatrixReady(dir),
|
|
170
178
|
};
|
|
171
179
|
|
|
172
180
|
const checks = [];
|
|
@@ -258,6 +258,47 @@ function checkChangeStates(root) {
|
|
|
258
258
|
return { pass: true, message: `${checked} change(s) have legal state values` };
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
// v0.13 §52 B4 测试门禁卫生巡检(C1-domain-policy 事件修复):
|
|
262
|
+
// 1. test_matrix_skipped=true 必须附 test_matrix_skip_reason(可审计豁免);
|
|
263
|
+
// 2. 状态引用的产物文件必须存在(arch_review_report / test_evidence_path)——
|
|
264
|
+
// C1 现场曾出现 arch_review_report 指向不存在的 auto-review.md。
|
|
265
|
+
// DP 时间戳顺序检查评估后未纳入:closing 后的 DP-7 时间戳晚于 last_transition 属合法,
|
|
266
|
+
// blanket 检查会误报,收益不抵风险。
|
|
267
|
+
function checkChangeTestGates(root) {
|
|
268
|
+
const changesDir = join(root, 'changes');
|
|
269
|
+
if (!existsSync(changesDir)) {
|
|
270
|
+
return { pass: true, message: 'no changes/ directory (skipped)' };
|
|
271
|
+
}
|
|
272
|
+
const dirs = readdirSync(changesDir).filter(f => {
|
|
273
|
+
try { return statSync(join(changesDir, f)).isDirectory(); } catch { return false; }
|
|
274
|
+
});
|
|
275
|
+
const issues = [];
|
|
276
|
+
let checked = 0;
|
|
277
|
+
for (const d of dirs) {
|
|
278
|
+
const changeDir = join(changesDir, d);
|
|
279
|
+
if (!existsSync(join(changeDir, '.team-flow.yaml'))) continue;
|
|
280
|
+
checked += 1;
|
|
281
|
+
const state = readState(changeDir);
|
|
282
|
+
if (state.test_matrix_skipped === 'true'
|
|
283
|
+
&& !(typeof state.test_matrix_skip_reason === 'string' && state.test_matrix_skip_reason.trim())) {
|
|
284
|
+
issues.push(`${d}: test_matrix_skipped=true without test_matrix_skip_reason`);
|
|
285
|
+
}
|
|
286
|
+
for (const field of ['arch_review_report', 'test_evidence_path']) {
|
|
287
|
+
const ref = state[field];
|
|
288
|
+
if (typeof ref === 'string' && ref.trim() && !existsSync(join(changeDir, ref))) {
|
|
289
|
+
issues.push(`${d}: ${field} references missing file '${ref}'`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (checked === 0) {
|
|
294
|
+
return { pass: true, message: 'no state files under changes/ (skipped)' };
|
|
295
|
+
}
|
|
296
|
+
if (issues.length > 0) {
|
|
297
|
+
return { pass: false, message: issues.join('; ') };
|
|
298
|
+
}
|
|
299
|
+
return { pass: true, message: `${checked} change(s) pass test-gate hygiene (skip reasons + artifact references)` };
|
|
300
|
+
}
|
|
301
|
+
|
|
261
302
|
export async function run(args) {
|
|
262
303
|
const root = process.cwd();
|
|
263
304
|
const config = loadConfig(root);
|
|
@@ -275,6 +316,7 @@ export async function run(args) {
|
|
|
275
316
|
['Node.js', checkNodeVersion()],
|
|
276
317
|
['Docs', checkDocs(root)],
|
|
277
318
|
['Change states', checkChangeStates(root)],
|
|
319
|
+
['Change test gates', checkChangeTestGates(root)],
|
|
278
320
|
];
|
|
279
321
|
|
|
280
322
|
// Config check
|
|
@@ -303,4 +345,4 @@ export async function run(args) {
|
|
|
303
345
|
}
|
|
304
346
|
}
|
|
305
347
|
|
|
306
|
-
export { checkVersionConsistency, checkHooks, checkCodexManifest, checkSkills, checkRuntimeDistribution, checkDist, checkRootPluginAuthor, checkNodeVersion, checkDocs, checkChangeStates };
|
|
348
|
+
export { checkVersionConsistency, checkHooks, checkCodexManifest, checkSkills, checkRuntimeDistribution, checkDist, checkRootPluginAuthor, checkNodeVersion, checkDocs, checkChangeStates, checkChangeTestGates };
|
|
@@ -22,6 +22,10 @@ export async function run(args) {
|
|
|
22
22
|
head: { type: 'string' },
|
|
23
23
|
report: { type: 'string' },
|
|
24
24
|
verdict: { type: 'string' },
|
|
25
|
+
// v0.13 §51.2:wave 级测试统计(可选证据字段,三个须同时提供)
|
|
26
|
+
'tests-total': { type: 'string' },
|
|
27
|
+
'tests-passed': { type: 'string' },
|
|
28
|
+
'tests-failed': { type: 'string' },
|
|
25
29
|
json: { type: 'boolean', default: false },
|
|
26
30
|
help: { type: 'boolean', default: false },
|
|
27
31
|
},
|
|
@@ -137,6 +141,7 @@ function recordAndPrintReview(changeDir, values) {
|
|
|
137
141
|
base: values.base,
|
|
138
142
|
head: values.head,
|
|
139
143
|
report: values.report,
|
|
144
|
+
tests: parseTestsStats(values),
|
|
140
145
|
});
|
|
141
146
|
print(values.json, { ok: true, wave: values.wave[0], receipt }, `Review for ${values.wave[0]} recorded: ${receipt.status}.`);
|
|
142
147
|
}
|
|
@@ -180,6 +185,24 @@ function requireOption(value, option) {
|
|
|
180
185
|
if (typeof value !== 'string' || !value.trim()) throw new Error(`${option} is required`);
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
// v0.13 §51.2:--tests-total/--tests-passed/--tests-failed 三个须同时提供且为非负整数。
|
|
189
|
+
function parseTestsStats(values) {
|
|
190
|
+
const raw = {
|
|
191
|
+
total: values['tests-total'],
|
|
192
|
+
passed: values['tests-passed'],
|
|
193
|
+
failed: values['tests-failed'],
|
|
194
|
+
};
|
|
195
|
+
const provided = Object.values(raw).filter(v => v !== undefined);
|
|
196
|
+
if (provided.length === 0) return undefined;
|
|
197
|
+
if (provided.length < 3) throw new Error('--tests-total, --tests-passed and --tests-failed must be provided together');
|
|
198
|
+
const parsed = {};
|
|
199
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
200
|
+
if (!/^\d+$/.test(String(value))) throw new Error(`--tests-${key} must be a non-negative integer`);
|
|
201
|
+
parsed[key] = parseInt(value, 10);
|
|
202
|
+
}
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
function requireSafeReason(reason) {
|
|
184
207
|
if (/[\p{Cc}\p{Zl}\p{Zp}]/u.test(reason)) {
|
|
185
208
|
throw new Error('--reason must not contain control characters or line separators');
|
|
@@ -202,5 +225,5 @@ function printHelp() {
|
|
|
202
225
|
tf execution plan <dir> --mode <mode> --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
|
|
203
226
|
tf execution show <dir> [--json]
|
|
204
227
|
tf execution revise <dir> --mode sdd --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
|
|
205
|
-
tf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail`);
|
|
228
|
+
tf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail [--tests-total N --tests-passed N --tests-failed N]`);
|
|
206
229
|
}
|
|
@@ -7,12 +7,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
// VALID_STATES 统一从 state-loader.mjs 引入(状态机合法值唯一真相源),不再本地硬编码。
|
|
8
8
|
// 来源:workflow-feedback 2026-08-01,#100005。
|
|
9
9
|
import { readState, writeState, updateField, rebuildState, VALID_STATES } from './state-loader.mjs';
|
|
10
|
-
import { computeArtifactsHash, computeContractHash } from './hash.mjs';
|
|
10
|
+
import { computeArtifactsHash, computeContractHash, computeTestMatrixHash } from './hash.mjs';
|
|
11
11
|
|
|
12
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
|
+
// v0.13 §50(BUG-A 政策修正):test_result 移出可手工设置字段——测试证据必须经
|
|
15
|
+
// tf test record 程序化记录,手工自述值不再被 tests-passing 门禁接受。
|
|
16
|
+
// v0.13 §48.2:test_matrix_skip_reason 可设置(显式 skip 的理由留痕)。
|
|
17
|
+
// schema_version 不可设置(仅 init 打戳,防止伪造豁免身份,v0.13 §48.1)。
|
|
14
18
|
const SETTABLE_FIELDS = [
|
|
15
|
-
'workflow', '
|
|
19
|
+
'workflow', 'batches_completed', 'spec_merged',
|
|
16
20
|
'dp_0_decisions', 'dp_0_confirmed', 'dp_0_timestamp', 'dp_0_result',
|
|
17
21
|
'dp_1_result', 'dp_1_timestamp', 'dp_1_decisions', 'dp_1_confirmed',
|
|
18
22
|
'dp_2_result', 'dp_2_timestamp', 'dp_2_decisions', 'dp_2_confirmed',
|
|
@@ -29,6 +33,8 @@ const SETTABLE_FIELDS = [
|
|
|
29
33
|
'dp_a_result', 'dp_a_timestamp', 'dp_a_adjustments',
|
|
30
34
|
// Compound engineering capture gate (v0.24.0 复利贯穿强制化)
|
|
31
35
|
'compound_skipped',
|
|
36
|
+
// Test matrix gate (v0.12 §45.4 + v0.13 §48.2)
|
|
37
|
+
'test_matrix_skipped', 'test_matrix_skip_reason',
|
|
32
38
|
];
|
|
33
39
|
|
|
34
40
|
export async function run(args) {
|
|
@@ -82,17 +88,27 @@ export async function run(args) {
|
|
|
82
88
|
if (!existsSync(changeDir)) {
|
|
83
89
|
mkdirSync(changeDir, { recursive: true });
|
|
84
90
|
}
|
|
91
|
+
// v0.13 §48.1:schema_version 只在 change 创建(状态文件尚不存在)时打戳。
|
|
92
|
+
// 对已有状态文件的目录重新 init 不得追加打戳——"字段缺失"是存量豁免键,
|
|
93
|
+
// 追加会把真存量 change 错误地纳入 v0.32 严格门禁。
|
|
94
|
+
const stateFileExisted = existsSync(join(changeDir, '.team-flow.yaml'));
|
|
85
95
|
const hash = computeArtifactsHash(changeDir);
|
|
86
96
|
const ch = computeContractHash(changeDir);
|
|
97
|
+
const tmh = computeTestMatrixHash(changeDir);
|
|
87
98
|
const state = readState(changeDir);
|
|
99
|
+
if (!stateFileExisted) {
|
|
100
|
+
state.schema_version = 1;
|
|
101
|
+
}
|
|
88
102
|
state.artifacts_hash = hash;
|
|
89
103
|
state.contract_hash = ch;
|
|
104
|
+
// v0.12 §42.5: test_matrix_hash is independent from artifacts_hash
|
|
105
|
+
state.test_matrix_hash = tmh;
|
|
90
106
|
state.last_transition = new Date().toISOString();
|
|
91
107
|
writeState(changeDir, state);
|
|
92
108
|
if (values.json) {
|
|
93
|
-
console.log(JSON.stringify({ ok: true, artifacts_hash: hash, contract_hash: ch }));
|
|
109
|
+
console.log(JSON.stringify({ ok: true, artifacts_hash: hash, contract_hash: ch, test_matrix_hash: tmh, schema_version: state.schema_version ?? null }));
|
|
94
110
|
} else {
|
|
95
|
-
console.log(`State initialized. artifacts_hash: ${hash}`);
|
|
111
|
+
console.log(`State initialized. artifacts_hash: ${hash}` + (tmh ? `, test_matrix_hash: ${tmh}` : '') + (state.schema_version != null ? `, schema_version: ${state.schema_version}` : ''));
|
|
96
112
|
}
|
|
97
113
|
break;
|
|
98
114
|
}
|
|
@@ -209,7 +225,7 @@ export async function run(args) {
|
|
|
209
225
|
break;
|
|
210
226
|
}
|
|
211
227
|
case 'rebuild': {
|
|
212
|
-
const state = rebuildState(changeDir, { computeArtifactsHash, computeContractHash });
|
|
228
|
+
const state = rebuildState(changeDir, { computeArtifactsHash, computeContractHash, computeTestMatrixHash });
|
|
213
229
|
if (values.json) {
|
|
214
230
|
console.log(JSON.stringify({ ok: true, state: state.state }));
|
|
215
231
|
} else {
|
|
@@ -235,6 +251,18 @@ export async function run(args) {
|
|
|
235
251
|
` tf state transition <change-dir 绝对路径> <目标状态>\n` +
|
|
236
252
|
` 合法状态:${VALID_STATES.join('/')}`
|
|
237
253
|
);
|
|
254
|
+
} else if (field === 'test_result') {
|
|
255
|
+
// v0.13 §50(BUG-A 政策修正):测试证据程序化,手工自述通道关闭。
|
|
256
|
+
console.error(
|
|
257
|
+
`⛔ 'test_result' 不再支持手工 set(v0.13 §50)。测试证据必须程序化记录:\n` +
|
|
258
|
+
` 运行项目测试套件后: tf test record <change-dir> --from <runner 输出文件>\n` +
|
|
259
|
+
` 确无自动化测试时显式跳过(可审计): tf state set <dir> test_matrix_skipped true\n` +
|
|
260
|
+
` + tf state set <dir> test_matrix_skip_reason "<理由>"`
|
|
261
|
+
);
|
|
262
|
+
} else if (field === 'schema_version') {
|
|
263
|
+
console.error(
|
|
264
|
+
`⛔ 'schema_version' 不可手工设置(v0.13 §48.1)。它只由 tf state init 在 change 创建时打戳,是测试门禁的存量豁免键。`
|
|
265
|
+
);
|
|
238
266
|
} else {
|
|
239
267
|
console.error(`⛔ Field '${field}' is not settable (use 'transition' for state, or check SETTABLE_FIELDS)`);
|
|
240
268
|
}
|
|
@@ -3,6 +3,8 @@ import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { join, basename } from 'node:path';
|
|
4
4
|
import { loadConfig } from './config-loader.mjs';
|
|
5
5
|
import { validateSpecPathLayout, relativeSpecPath } from './spec-paths.mjs';
|
|
6
|
+
import { readState } from './state-loader.mjs';
|
|
7
|
+
import { computeTestMatrixHash } from './hash.mjs';
|
|
6
8
|
|
|
7
9
|
async function getValidator() {
|
|
8
10
|
const mod = await import('../../dist/index.js');
|
|
@@ -94,6 +96,46 @@ export async function run(args) {
|
|
|
94
96
|
if (!report.valid) hasErrors = true;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
// v0.13 §52 B1:test-matrix 双向互锁(contract '## Test Matrix' 段 ↔ test-matrix.md)
|
|
100
|
+
// 非存量(有 schema_version 打戳)且未显式 skip 的 change:
|
|
101
|
+
// 契约声明段 → 矩阵文件必须存在非空;矩阵文件存在 → 契约必须声明段(孤儿矩阵)。
|
|
102
|
+
// 两者皆无不算错(矩阵是否必产由 test-matrix-ready 门禁在转换时强制)。
|
|
103
|
+
const contractForMatrixPath = join(changeDir, 'execution-contract.md');
|
|
104
|
+
if (existsSync(contractForMatrixPath)) {
|
|
105
|
+
const stateForMatrix = readState(changeDir);
|
|
106
|
+
const legacyChange = stateForMatrix.schema_version == null;
|
|
107
|
+
const matrixSkipped = stateForMatrix.test_matrix_skipped === 'true';
|
|
108
|
+
const contractContent = readFileSync(contractForMatrixPath, 'utf-8');
|
|
109
|
+
const declaresMatrix = contractContent.includes('## Test Matrix');
|
|
110
|
+
const matrixPath = join(changeDir, 'test-matrix.md');
|
|
111
|
+
const matrixExists = existsSync(matrixPath) && readFileSync(matrixPath, 'utf-8').trim().length > 0;
|
|
112
|
+
const issues = [];
|
|
113
|
+
if (!legacyChange && !matrixSkipped) {
|
|
114
|
+
if (declaresMatrix && !matrixExists) {
|
|
115
|
+
issues.push({ level: 'ERROR', path: 'test-matrix.md', message: 'contract declares ## Test Matrix but test-matrix.md is missing or empty — generate the matrix (contract-builder) or remove the section' });
|
|
116
|
+
}
|
|
117
|
+
if (!declaresMatrix && matrixExists) {
|
|
118
|
+
issues.push({ level: 'ERROR', path: 'execution-contract.md', message: 'test-matrix.md exists but contract does not declare ## Test Matrix — add the section or remove the orphaned matrix' });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (declaresMatrix && matrixExists) {
|
|
122
|
+
const computed = computeTestMatrixHash(changeDir);
|
|
123
|
+
if (!stateForMatrix.test_matrix_hash) {
|
|
124
|
+
issues.push({ level: 'WARNING', path: 'test-matrix.md', message: "test_matrix_hash not recorded — run 'tf state rebuild' to capture the matrix hash" });
|
|
125
|
+
} else if (stateForMatrix.test_matrix_hash !== computed) {
|
|
126
|
+
issues.push({ level: 'WARNING', path: 'test-matrix.md', message: "test_matrix_hash drifted from test-matrix.md — run 'tf state rebuild'" });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (issues.length > 0) {
|
|
130
|
+
printReport('test-matrix cross-lock', {
|
|
131
|
+
valid: issues.filter(i => i.level === 'ERROR').length === 0,
|
|
132
|
+
issues,
|
|
133
|
+
summary: { errors: issues.filter(i => i.level === 'ERROR').length, warnings: issues.filter(i => i.level === 'WARNING').length, info: 0 },
|
|
134
|
+
});
|
|
135
|
+
if (issues.some(i => i.level === 'ERROR')) hasErrors = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
97
139
|
console.log('');
|
|
98
140
|
if (hasErrors) {
|
|
99
141
|
console.log('❌ Validation failed with errors.');
|
|
@@ -132,12 +132,26 @@ export function recordReview(changeDir, waveId, receipt) {
|
|
|
132
132
|
for (const field of ['base', 'head']) requireText(receipt?.[field], `receipt.${field}`);
|
|
133
133
|
const report = validateReviewReportEvidence(changeDir, receipt?.report);
|
|
134
134
|
const { base, head } = validateReviewRange(changeDir, receipt.base, receipt.head);
|
|
135
|
+
// v0.13 §51.2:测试统计为可选证据字段(提供时必须为非负整数)。
|
|
136
|
+
// 硬门禁在入口(test-matrix-ready)与 closing(tests-passing),此处只沉淀证据,
|
|
137
|
+
// 避免单 wave 零测试造成 BUG-A 式过严死锁(DDL/脚手架类 wave 可能合理无测试)。
|
|
138
|
+
let tests;
|
|
139
|
+
if (receipt?.tests !== undefined) {
|
|
140
|
+
if (!isObject(receipt.tests)) throw new Error('receipt.tests must be an object');
|
|
141
|
+
for (const key of ['total', 'passed', 'failed']) {
|
|
142
|
+
if (!Number.isInteger(receipt.tests[key]) || receipt.tests[key] < 0) {
|
|
143
|
+
throw new Error(`receipt.tests.${key} must be a non-negative integer`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
tests = { total: receipt.tests.total, passed: receipt.tests.passed, failed: receipt.tests.failed };
|
|
147
|
+
}
|
|
135
148
|
|
|
136
149
|
const savedReceipt = {
|
|
137
150
|
status: receipt.status,
|
|
138
151
|
base,
|
|
139
152
|
head,
|
|
140
153
|
report,
|
|
154
|
+
...(tests ? { tests } : {}),
|
|
141
155
|
plan_hash: plan.hash,
|
|
142
156
|
plan_revision: plan.revision,
|
|
143
157
|
recorded_at: new Date().toISOString(),
|
|
@@ -252,6 +266,15 @@ function validateReviewRange(changeDir, base, head) {
|
|
|
252
266
|
const gitRoot = getGitRoot(changeDir);
|
|
253
267
|
const resolvedBase = resolveGitCommit(gitRoot, base, 'base');
|
|
254
268
|
const resolvedHead = resolveGitCommit(gitRoot, head, 'head');
|
|
269
|
+
// v0.13 §51.1:禁止空 diff review(base === head)。
|
|
270
|
+
// C1-domain-policy 现场曾出现 6 个 receipt 全部 base==head==初始 commit,
|
|
271
|
+
// 即 review 对空 diff 进行。本校验同时强制"每 wave 落 commit"的纪律。
|
|
272
|
+
if (resolvedBase === resolvedHead) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
'Review receipt base must differ from head — head must contain new commits over base; '
|
|
275
|
+
+ 'commit the wave changes before recording a review'
|
|
276
|
+
);
|
|
277
|
+
}
|
|
255
278
|
try {
|
|
256
279
|
execFileSync('git', ['-C', gitRoot, 'merge-base', '--is-ancestor', resolvedBase, resolvedHead], {
|
|
257
280
|
stdio: 'ignore',
|
package/scripts/lib/hash.mjs
CHANGED
|
@@ -67,6 +67,17 @@ export function computeContractHash(changeDir) {
|
|
|
67
67
|
return `sha256:${hash.digest('hex')}`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Compute SHA256 hash of test-matrix.md alone (v0.12 §42.5).
|
|
71
|
+
// Independent from artifacts_hash to avoid self-circulation:
|
|
72
|
+
// modify matrix → contract stale → rollback bridging → regenerate matrix.
|
|
73
|
+
export function computeTestMatrixHash(changeDir) {
|
|
74
|
+
const matrix = path.join(changeDir, 'test-matrix.md');
|
|
75
|
+
if (!fs.existsSync(matrix)) return null;
|
|
76
|
+
const hash = crypto.createHash('sha256');
|
|
77
|
+
hash.update(fs.readFileSync(matrix, 'utf-8'));
|
|
78
|
+
return `sha256:${hash.digest('hex')}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
// Fast staleness check: compare stored artifacts_hash against current.
|
|
71
82
|
export function isContractFresh(changeDir, stateLoader) {
|
|
72
83
|
const stateFile = path.join(changeDir, '.team-flow.yaml');
|
|
@@ -61,6 +61,14 @@ const BUILTIN_DEFAULTS = {
|
|
|
61
61
|
dp_a_adjustments: null,
|
|
62
62
|
// Compound engineering capture gate (v0.24.0 复利贯穿强制化)
|
|
63
63
|
compound_skipped: null,
|
|
64
|
+
// Test matrix gate (v0.12 §42.5 + §45.4; v0.13 §48 补 skip 理由字段)
|
|
65
|
+
test_matrix_hash: null,
|
|
66
|
+
test_matrix_skipped: null,
|
|
67
|
+
test_matrix_skip_reason: null,
|
|
68
|
+
// Test evidence (v0.13 §50:tf test record 落盘的 runner 输出证据路径)
|
|
69
|
+
test_evidence_path: null,
|
|
70
|
+
// 注意:schema_version 故意不在 BUILTIN_DEFAULTS 中(v0.13 §48.1)——
|
|
71
|
+
// 它只由 `tf state init` 在 change 创建时打戳,字段缺失本身就是"存量 change"信号。
|
|
64
72
|
};
|
|
65
73
|
|
|
66
74
|
/**
|
|
@@ -105,6 +113,11 @@ export function writeState(changeDir, state) {
|
|
|
105
113
|
lines.push(`state: ${state.state || 'exploring'}`);
|
|
106
114
|
lines.push(`workflow: ${state.workflow || 'auto'}`);
|
|
107
115
|
lines.push(`revision: ${state.revision ?? 'null'}`);
|
|
116
|
+
// v0.13 §48.1:schema_version 仅由 tf state init 在 change 创建时打戳;
|
|
117
|
+
// 缺失 = 存量 change(测试门禁豁免键)。rebuild/set 不追加,故条件序列化。
|
|
118
|
+
if (state.schema_version != null) {
|
|
119
|
+
lines.push(`schema_version: ${state.schema_version}`);
|
|
120
|
+
}
|
|
108
121
|
lines.push('');
|
|
109
122
|
lines.push('# === Hashes (fast staleness detection) ===');
|
|
110
123
|
lines.push(`artifacts_hash: ${state.artifacts_hash ?? 'null'}`);
|
|
@@ -162,6 +175,14 @@ export function writeState(changeDir, state) {
|
|
|
162
175
|
lines.push('');
|
|
163
176
|
lines.push('# === Compound engineering capture gate (v0.24.0) ===');
|
|
164
177
|
lines.push(`compound_skipped: ${state.compound_skipped ?? 'null'}`);
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push('# === Test matrix gate (v0.12 §42.5 + v0.13 §48) ===');
|
|
180
|
+
lines.push(`test_matrix_hash: ${state.test_matrix_hash ?? 'null'}`);
|
|
181
|
+
lines.push(`test_matrix_skipped: ${state.test_matrix_skipped ?? 'null'}`);
|
|
182
|
+
lines.push(`test_matrix_skip_reason: ${state.test_matrix_skip_reason ?? 'null'}`);
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push('# === Test evidence (v0.13 §50) ===');
|
|
185
|
+
lines.push(`test_evidence_path: ${state.test_evidence_path ?? 'null'}`);
|
|
165
186
|
|
|
166
187
|
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
|
|
167
188
|
}
|
|
@@ -179,10 +200,14 @@ export function updateField(changeDir, field, value) {
|
|
|
179
200
|
* Rebuild state file from artifacts — recomputes hashes.
|
|
180
201
|
* Requires hash functions to be passed in (avoids circular dependency).
|
|
181
202
|
*/
|
|
182
|
-
export function rebuildState(changeDir, { computeArtifactsHash, computeContractHash }) {
|
|
203
|
+
export function rebuildState(changeDir, { computeArtifactsHash, computeContractHash, computeTestMatrixHash }) {
|
|
183
204
|
const state = readState(changeDir);
|
|
184
205
|
state.artifacts_hash = computeArtifactsHash(changeDir);
|
|
185
206
|
state.contract_hash = computeContractHash(changeDir);
|
|
207
|
+
// v0.12 §42.5: test_matrix_hash is independent from artifacts_hash
|
|
208
|
+
if (computeTestMatrixHash) {
|
|
209
|
+
state.test_matrix_hash = computeTestMatrixHash(changeDir);
|
|
210
|
+
}
|
|
186
211
|
writeState(changeDir, state);
|
|
187
212
|
return state;
|
|
188
213
|
}
|