@xulthekl/team-flow 0.31.0 → 0.32.2
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 +1 -1
- 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 +3 -3
- package/CHANGELOG.md +69 -0
- package/GEMINI.md +1 -1
- package/HANDOFF.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +2 -2
- 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/pre-tool-use-guard +43 -2
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +6 -2
- package/plugin.json +1 -1
- package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
- package/scripts/guard/checks/test-matrix-complete.mjs +54 -30
- package/scripts/guard/checks/test-matrix-ready.mjs +60 -0
- package/scripts/guard/checks/tests-passing.mjs +112 -24
- package/scripts/guard/guard.mjs +11 -2
- package/scripts/lib/cmd-doctor.mjs +43 -1
- package/scripts/lib/cmd-execution.mjs +24 -1
- package/scripts/lib/cmd-state.mjs +28 -5
- package/scripts/lib/cmd-validate.mjs +42 -0
- package/scripts/lib/cmd-version.mjs +26 -21
- package/scripts/lib/execution-plan.mjs +23 -0
- package/scripts/lib/state-loader.mjs +16 -2
- package/scripts/lib/test-record.mjs +212 -0
- package/scripts/team-flow.mjs +3 -0
- package/skills/contract-builder/SKILL.md +9 -10
- package/skills/release-archivist/SKILL.md +16 -9
- package/skills/session-handoff/SKILL.md +1 -0
- package/skills/workflow-start/SKILL.md +5 -3
- package/skills/workflow-start/references/routing-rules.md +10 -6
- 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
|
@@ -1,60 +1,84 @@
|
|
|
1
1
|
// scripts/guard/checks/test-matrix-complete.mjs — verify test matrix exists and is consistent
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
//
|
|
3
|
+
// v0.13 §48 重写(C1-domain-policy 事件修复,RC-1/RC-3):
|
|
4
|
+
// 豁免键由 contract 内容改为 state schema_version;删除 v0.12 的两条结构性豁免:
|
|
5
|
+
// ✗ "contract 无 '## Test Matrix' 段 → PASS"(内容型豁免,新 change 漏生成矩阵同样命中)
|
|
6
|
+
// ✗ "stored hash 为 null 时短路跳过一致性校验"(矩阵生成后未 rebuild 即无保护)
|
|
7
|
+
// 判定顺序见设计增强方案 v0.13 §48.3。
|
|
4
8
|
import { readState } from '../../lib/state-loader.mjs';
|
|
5
9
|
import { computeTestMatrixHash } from '../../lib/hash.mjs';
|
|
10
|
+
import {
|
|
11
|
+
isLegacyChange,
|
|
12
|
+
skipMissingReason,
|
|
13
|
+
SKIP_REASON_HINT,
|
|
14
|
+
contractDeclaresTestMatrix,
|
|
15
|
+
readTestMatrixFile,
|
|
16
|
+
} from './test-gate-exemptions.mjs';
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
|
-
* Check that test-matrix.md exists and its hash is consistent with state.
|
|
9
|
-
*
|
|
10
|
-
* Exemption paths (v0.12 §45.3):
|
|
11
|
-
* 1. Legacy: execution-contract.md has no "## Test Matrix" section → PASS
|
|
12
|
-
* 2. Explicit skip: test_matrix_skipped === 'true' → PASS
|
|
13
|
-
* 3. test-matrix.md exists and hash matches → PASS
|
|
14
|
-
* 4. test-matrix.md missing or hash mismatch → FAIL
|
|
15
|
-
*
|
|
16
19
|
* Returns { pass, failures[], reason? }.
|
|
17
20
|
*/
|
|
18
21
|
export function checkTestMatrixComplete(changeDir) {
|
|
19
22
|
const state = readState(changeDir);
|
|
20
23
|
|
|
21
|
-
//
|
|
22
|
-
if (state
|
|
23
|
-
return { pass: true, failures: [], reason: '
|
|
24
|
+
// Step 1 — legacy:v0.32.0 之前初始化的 change(无 schema_version 打戳)保留旧行为。
|
|
25
|
+
if (isLegacyChange(state)) {
|
|
26
|
+
return { pass: true, failures: [], reason: 'legacy change — initialized before v0.32.0' };
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!contract.includes('## Test Matrix')) {
|
|
31
|
-
return { pass: true, failures: [], reason: 'legacy contract — no test matrix section' };
|
|
29
|
+
// Step 2 — 显式 skip:必须附理由(可审计豁免)。
|
|
30
|
+
if (state.test_matrix_skipped === 'true') {
|
|
31
|
+
if (skipMissingReason(state)) {
|
|
32
|
+
return { pass: false, failures: [SKIP_REASON_HINT] };
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
// No contract at all — nothing to check
|
|
35
|
-
return { pass: true, failures: [], reason: 'no execution contract — skipping test matrix check' };
|
|
34
|
+
return { pass: true, failures: [], reason: `explicitly skipped: ${state.test_matrix_skip_reason}` };
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
37
|
+
// Step 3 — 非存量 change 的契约必须声明矩阵(v0.13:内容型豁免删除)。
|
|
38
|
+
const contract = contractDeclaresTestMatrix(changeDir);
|
|
39
|
+
if (!contract.exists) {
|
|
41
40
|
return {
|
|
42
41
|
pass: false,
|
|
43
|
-
failures: ['
|
|
42
|
+
failures: ['execution-contract.md is missing — a non-legacy change must have a contract declaring ## Test Matrix'],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (!contract.declares) {
|
|
46
|
+
return {
|
|
47
|
+
pass: false,
|
|
48
|
+
failures: [
|
|
49
|
+
'non-legacy contract must declare ## Test Matrix — return to bridging so contract-builder generates test-matrix.md, '
|
|
50
|
+
+ 'or explicitly skip (test_matrix_skipped=true + test_matrix_skip_reason)',
|
|
51
|
+
],
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
// Step 4 — test-matrix.md 存在且非空。
|
|
56
|
+
const matrix = readTestMatrixFile(changeDir);
|
|
57
|
+
if (!matrix.exists) {
|
|
58
|
+
return {
|
|
59
|
+
pass: false,
|
|
60
|
+
failures: ['test-matrix.md is missing — contract declares ## Test Matrix but no matrix file found'],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (matrix.empty) {
|
|
49
64
|
return {
|
|
50
65
|
pass: false,
|
|
51
66
|
failures: ['test-matrix.md is empty — must contain at least a Summary section and Cases table'],
|
|
52
67
|
};
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
//
|
|
70
|
+
// Step 5 — hash 必须已记录(v0.13:删除 state.test_matrix_hash && 的空值短路)。
|
|
71
|
+
// 矩阵在 bridging 阶段生成(晚于 state init),contract-builder 生成后必须 tf state rebuild 捕获 hash。
|
|
56
72
|
const computed = computeTestMatrixHash(changeDir);
|
|
57
|
-
if (state.test_matrix_hash
|
|
73
|
+
if (!state.test_matrix_hash) {
|
|
74
|
+
return {
|
|
75
|
+
pass: false,
|
|
76
|
+
failures: ["test_matrix_hash not recorded — run 'tf state rebuild' after generating test-matrix.md"],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 6 — hash 一致性。
|
|
81
|
+
if (state.test_matrix_hash !== computed) {
|
|
58
82
|
return {
|
|
59
83
|
pass: false,
|
|
60
84
|
failures: [
|
|
@@ -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
|
@@ -15,14 +15,22 @@ 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
17
|
import { checkTestMatrixComplete } from './checks/test-matrix-complete.mjs';
|
|
18
|
+
import { checkTestMatrixReady } from './checks/test-matrix-ready.mjs';
|
|
18
19
|
|
|
19
20
|
// Transition matrix: <from>:<to> → required check dimensions
|
|
20
21
|
const TRANSITION_CHECKS = {
|
|
21
22
|
// Forward transitions
|
|
22
|
-
|
|
23
|
+
// v0.32.2 (P3-10): artifacts-exist removed from exploring:specifying —
|
|
24
|
+
// the four planning artifacts are PRODUCED by spec-writer during specifying,
|
|
25
|
+
// so requiring them before the transition was a chicken-and-egg deadlock
|
|
26
|
+
// (C1 事件:主代理被此门禁逼出绕行行为)。Artifact completeness is enforced
|
|
27
|
+
// by specifying:bridging (artifacts-exist remains there, single owner).
|
|
28
|
+
'exploring:specifying': ['arch-design'],
|
|
23
29
|
'specifying:bridging': ['artifacts-exist', 'schema-valid'],
|
|
24
30
|
'bridging:approved-for-build': ['artifacts-exist', 'schema-valid', 'contract-fresh', 'dp-gate-passed'],
|
|
25
|
-
|
|
31
|
+
// v0.13 §49:test-matrix-ready 门禁前移——full 模式进入 executing 前强制测试准备度
|
|
32
|
+
//(矩阵存在非空 OR 显式 skip 附理由;legacy 豁免)。hotfix/tweak 沿用 §45.3 豁免,不挂。
|
|
33
|
+
'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed', 'execution-plan-ready', 'test-matrix-ready'],
|
|
26
34
|
'executing:closing': ['tasks-complete', 'tests-passing', 'specs-merged', 'execution-plan-ready', 'execution-reviews-passed', 'compound-captured', 'test-matrix-complete'],
|
|
27
35
|
|
|
28
36
|
// Debugging side-path
|
|
@@ -171,6 +179,7 @@ async function main() {
|
|
|
171
179
|
'arch-design': (dir) => checkArchDesign(dir),
|
|
172
180
|
'compound-captured': (dir) => checkCompoundCaptured(dir),
|
|
173
181
|
'test-matrix-complete': (dir) => checkTestMatrixComplete(dir),
|
|
182
|
+
'test-matrix-ready': (dir) => checkTestMatrixReady(dir),
|
|
174
183
|
};
|
|
175
184
|
|
|
176
185
|
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
|
}
|
|
@@ -11,8 +11,12 @@ import { computeArtifactsHash, computeContractHash, computeTestMatrixHash } from
|
|
|
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,8 +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',
|
|
32
|
-
// Test matrix gate (v0.12 §45.4)
|
|
33
|
-
'test_matrix_skipped',
|
|
36
|
+
// Test matrix gate (v0.12 §45.4 + v0.13 §48.2)
|
|
37
|
+
'test_matrix_skipped', 'test_matrix_skip_reason',
|
|
34
38
|
];
|
|
35
39
|
|
|
36
40
|
export async function run(args) {
|
|
@@ -84,10 +88,17 @@ export async function run(args) {
|
|
|
84
88
|
if (!existsSync(changeDir)) {
|
|
85
89
|
mkdirSync(changeDir, { recursive: true });
|
|
86
90
|
}
|
|
91
|
+
// v0.13 §48.1:schema_version 只在 change 创建(状态文件尚不存在)时打戳。
|
|
92
|
+
// 对已有状态文件的目录重新 init 不得追加打戳——"字段缺失"是存量豁免键,
|
|
93
|
+
// 追加会把真存量 change 错误地纳入 v0.32 严格门禁。
|
|
94
|
+
const stateFileExisted = existsSync(join(changeDir, '.team-flow.yaml'));
|
|
87
95
|
const hash = computeArtifactsHash(changeDir);
|
|
88
96
|
const ch = computeContractHash(changeDir);
|
|
89
97
|
const tmh = computeTestMatrixHash(changeDir);
|
|
90
98
|
const state = readState(changeDir);
|
|
99
|
+
if (!stateFileExisted) {
|
|
100
|
+
state.schema_version = 1;
|
|
101
|
+
}
|
|
91
102
|
state.artifacts_hash = hash;
|
|
92
103
|
state.contract_hash = ch;
|
|
93
104
|
// v0.12 §42.5: test_matrix_hash is independent from artifacts_hash
|
|
@@ -95,9 +106,9 @@ export async function run(args) {
|
|
|
95
106
|
state.last_transition = new Date().toISOString();
|
|
96
107
|
writeState(changeDir, state);
|
|
97
108
|
if (values.json) {
|
|
98
|
-
console.log(JSON.stringify({ ok: true, artifacts_hash: hash, contract_hash: ch, test_matrix_hash: tmh }));
|
|
109
|
+
console.log(JSON.stringify({ ok: true, artifacts_hash: hash, contract_hash: ch, test_matrix_hash: tmh, schema_version: state.schema_version ?? null }));
|
|
99
110
|
} else {
|
|
100
|
-
console.log(`State initialized. artifacts_hash: ${hash}` + (tmh ? `, test_matrix_hash: ${tmh}` : ''));
|
|
111
|
+
console.log(`State initialized. artifacts_hash: ${hash}` + (tmh ? `, test_matrix_hash: ${tmh}` : '') + (state.schema_version != null ? `, schema_version: ${state.schema_version}` : ''));
|
|
101
112
|
}
|
|
102
113
|
break;
|
|
103
114
|
}
|
|
@@ -240,6 +251,18 @@ export async function run(args) {
|
|
|
240
251
|
` tf state transition <change-dir 绝对路径> <目标状态>\n` +
|
|
241
252
|
` 合法状态:${VALID_STATES.join('/')}`
|
|
242
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
|
+
);
|
|
243
266
|
} else {
|
|
244
267
|
console.error(`⛔ Field '${field}' is not settable (use 'transition' for state, or check SETTABLE_FIELDS)`);
|
|
245
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.');
|