@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.
Files changed (101) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.cursor-plugin/marketplace.json +2 -2
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/AGENTS.md +5 -4
  9. package/CHANGELOG.md +85 -0
  10. package/GEMINI.md +1 -1
  11. package/HANDOFF.md +1 -1
  12. package/INSTALL.md +1 -1
  13. package/README.md +3 -3
  14. package/agents/build-executor.md +1 -0
  15. package/agents/contract-builder.md +6 -1
  16. package/docs/README_en.md +1 -1
  17. package/docs/solutions/INDEX.md +2 -0
  18. package/docs/solutions/cross-phase/2026-08-01-no-summary.md +17 -0
  19. package/docs/solutions/cross-phase/2026-08-03-no-summary.md +17 -0
  20. package/gemini-extension.json +2 -2
  21. package/hooks/session-start +2 -2
  22. package/llms.txt +1 -1
  23. package/package.json +6 -2
  24. package/plugin.json +2 -2
  25. package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
  26. package/scripts/guard/checks/test-matrix-complete.mjs +91 -0
  27. package/scripts/guard/checks/test-matrix-ready.mjs +60 -0
  28. package/scripts/guard/checks/tests-passing.mjs +112 -24
  29. package/scripts/guard/guard.mjs +10 -2
  30. package/scripts/lib/cmd-doctor.mjs +43 -1
  31. package/scripts/lib/cmd-execution.mjs +24 -1
  32. package/scripts/lib/cmd-state.mjs +33 -5
  33. package/scripts/lib/cmd-validate.mjs +42 -0
  34. package/scripts/lib/execution-plan.mjs +23 -0
  35. package/scripts/lib/hash.mjs +11 -0
  36. package/scripts/lib/state-loader.mjs +26 -1
  37. package/scripts/lib/test-matrix-export.mjs +231 -0
  38. package/scripts/lib/test-merge.mjs +540 -0
  39. package/scripts/lib/test-record.mjs +212 -0
  40. package/scripts/team-flow.mjs +9 -0
  41. package/skills/build-executor/implementer-prompt.md +38 -3
  42. package/skills/code-reviewer/SKILL.md +28 -1
  43. package/skills/code-reviewer/code-reviewer-prompt.md +10 -0
  44. package/skills/contract-builder/SKILL.md +75 -0
  45. package/skills/release-archivist/SKILL.md +50 -10
  46. package/skills/session-handoff/SKILL.md +1 -0
  47. package/skills/spec-writer/SKILL.md +3 -1
  48. package/skills/test-strategy/SKILL.md +70 -0
  49. package/skills/test-strategy/references/adversarial-patterns.md +0 -0
  50. package/skills/test-strategy/references/complexity-grading.md +137 -0
  51. package/skills/test-strategy/references/design-methods-detail.md +183 -0
  52. package/skills/workflow-orchestrator/references/s1-path-router.md +4 -0
  53. package/skills/workflow-start/SKILL.md +5 -3
  54. package/skills/workflow-start/references/routing-rules.md +21 -0
  55. package/tests/e2e.test.mjs +0 -266
  56. package/tests/lib/cmd-audit.test.mjs +0 -202
  57. package/tests/lib/cmd-checkpoint.test.mjs +0 -98
  58. package/tests/lib/cmd-config.test.mjs +0 -101
  59. package/tests/lib/cmd-doctor.test.mjs +0 -450
  60. package/tests/lib/cmd-execution.test.mjs +0 -647
  61. package/tests/lib/cmd-handoff.test.mjs +0 -137
  62. package/tests/lib/cmd-inject.test.mjs +0 -218
  63. package/tests/lib/cmd-install-workbuddy.test.mjs +0 -113
  64. package/tests/lib/cmd-install-zcode.test.mjs +0 -42
  65. package/tests/lib/cmd-isolate.test.mjs +0 -68
  66. package/tests/lib/cmd-list.test.mjs +0 -127
  67. package/tests/lib/cmd-runtime.test.mjs +0 -83
  68. package/tests/lib/cmd-state-missing.test.mjs +0 -65
  69. package/tests/lib/cmd-state.test.mjs +0 -365
  70. package/tests/lib/cmd-sync-paths.test.mjs +0 -84
  71. package/tests/lib/cmd-validate-paths.test.mjs +0 -90
  72. package/tests/lib/config-loader.test.mjs +0 -175
  73. package/tests/lib/ensure-branch.test.mjs +0 -110
  74. package/tests/lib/execution-control-plane.test.mjs +0 -168
  75. package/tests/lib/execution-plan.test.mjs +0 -474
  76. package/tests/lib/guard-compound-captured.test.mjs +0 -92
  77. package/tests/lib/guard-specs-merged.test.mjs +0 -110
  78. package/tests/lib/guard-tests-passing.test.mjs +0 -114
  79. package/tests/lib/guard-transitions.test.mjs +0 -262
  80. package/tests/lib/guard.test.mjs +0 -613
  81. package/tests/lib/hash.test.mjs +0 -138
  82. package/tests/lib/infer-workflow.test.mjs +0 -161
  83. package/tests/lib/install-git-hooks.test.mjs +0 -20
  84. package/tests/lib/marketplace-release-docs.test.mjs +0 -31
  85. package/tests/lib/minimality-discipline.test.mjs +0 -37
  86. package/tests/lib/model-profiles-docs.test.mjs +0 -33
  87. package/tests/lib/node20-compatibility.test.mjs +0 -73
  88. package/tests/lib/node20-test-entry.test.mjs +0 -17
  89. package/tests/lib/platform-runtime-distribution.test.mjs +0 -157
  90. package/tests/lib/raw-mode-smoke.test.mjs +0 -42
  91. package/tests/lib/sdd-overlay.test.mjs +0 -118
  92. package/tests/lib/solutions-capture.test.mjs +0 -108
  93. package/tests/lib/solutions-index-gen.test.mjs +0 -147
  94. package/tests/lib/solutions-inject.test.mjs +0 -115
  95. package/tests/lib/solutions-promote.test.mjs +0 -200
  96. package/tests/lib/spec-paths.test.mjs +0 -80
  97. package/tests/lib/state-loader.test.mjs +0 -251
  98. package/tests/lib/token-baseline.test.mjs +0 -72
  99. package/tests/lib/token-rules.test.mjs +0 -94
  100. package/tests/lib/verify-marketplace-release.test.mjs +0 -128
  101. 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 verification outcome is recorded as pass
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
- * Check that .team-flow.yaml records a passing verification outcome.
6
- *
7
- * Historically only `test_result: pass` was accepted, but release-archivist
8
- * records the verification result in `dp_6_result` (e.g. "pass: ..."). The
9
- * closing transition was unreachable because nothing ever wrote `test_result`,
10
- * so we accept either signal. See BUG-A (issue #28 root cause).
11
- *
12
- * Returns { pass, failures[] }.
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
- const markers = [state.test_result, state.dp_6_result];
17
- const passed = markers.some((v) => {
18
- if (typeof v !== 'string') return false;
19
- const normalized = v.trim().replace(/^["']|["']$/g, '');
20
- return normalized.toLowerCase().startsWith('pass');
21
- });
22
- if (passed) {
23
- return { pass: true, failures: [] };
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
- return {
26
- pass: false,
27
- failures: [
28
- `verification outcome missing: 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.`,
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
+ }
@@ -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
- 'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed', 'execution-plan-ready'],
25
- 'executing:closing': ['tasks-complete', 'tests-passing', 'specs-merged', 'execution-plan-ready', 'execution-reviews-passed', 'compound-captured'],
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', 'test_result', 'batches_completed', 'spec_merged',
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',
@@ -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
  }