@xulthekl/team-flow 0.62.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +1 -1
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/.github/workflows/ci.yml +2 -0
  9. package/CHANGELOG.md +67 -0
  10. package/GEMINI.md +1 -1
  11. package/INSTALL.md +1 -1
  12. package/README.md +1 -1
  13. package/docs/README_en.md +1 -1
  14. package/docs/decision-points.md +8 -0
  15. package/docs/state-machine.md +4 -1
  16. package/docs/team-flow /344/275/277/347/224/250/350/257/264/346/230/216/357/274/210/347/240/224/345/217/221/345/233/242/351/230/237/347/211/210/357/274/211.md" +4 -4
  17. package/gemini-extension.json +1 -1
  18. package/hooks/session-start +2 -2
  19. package/llms.txt +1 -1
  20. package/package.json +1 -1
  21. package/plugin.json +1 -1
  22. package/scripts/guard/checks/_fs-utils.mjs +18 -0
  23. package/scripts/guard/checks/arch-design-light.mjs +39 -0
  24. package/scripts/guard/checks/arch-merged-light.mjs +67 -0
  25. package/scripts/guard/checks/arch-snapshot-light.mjs +30 -0
  26. package/scripts/guard/checks/artifacts-planned.mjs +38 -0
  27. package/scripts/guard/checks/compound-writeback-light.mjs +45 -0
  28. package/scripts/guard/checks/contract-fresh.mjs +48 -4
  29. package/scripts/guard/checks/cross-change-consistency-light.mjs +75 -0
  30. package/scripts/guard/checks/direct-short-path.mjs +52 -0
  31. package/scripts/guard/checks/direct-test-result.mjs +30 -0
  32. package/scripts/guard/checks/execution-plan-ready.mjs +7 -1
  33. package/scripts/guard/checks/execution-reviews-passed-light.mjs +28 -0
  34. package/scripts/guard/checks/gates-probed.mjs +175 -0
  35. package/scripts/guard/checks/lightweight-completion-evidence.mjs +27 -0
  36. package/scripts/guard/checks/specs-merged.mjs +25 -1
  37. package/scripts/guard/checks/test-matrix-complete.mjs +27 -1
  38. package/scripts/guard/checks/test-matrix-ready.mjs +28 -1
  39. package/scripts/guard/checks/test-merged-light.mjs +36 -0
  40. package/scripts/guard/guard.mjs +119 -14
  41. package/scripts/infer-workflow.mjs +35 -4
  42. package/scripts/lib/arch-merge.mjs +20 -4
  43. package/scripts/lib/cmd-execution.mjs +44 -1
  44. package/scripts/lib/cmd-state.mjs +98 -4
  45. package/scripts/lib/execution-plan.mjs +3 -1
  46. package/scripts/lib/state-loader.mjs +55 -0
  47. package/scripts/lib/surface-scan.mjs +156 -0
  48. package/scripts/lib/test-merge.mjs +10 -2
  49. package/scripts/team-flow.mjs +3 -3
  50. package/skills/build-executor/SKILL.md +6 -11
  51. package/skills/build-executor/references/wave-delivery-selfcheck.md +92 -0
  52. package/skills/clean-code/SKILL.md +1 -1
  53. package/skills/code-reviewer/SKILL.md +4 -0
  54. package/skills/contract-builder/SKILL.md +21 -0
  55. package/skills/contract-builder/references/bridging-gate-dry-run.md +89 -0
  56. package/skills/contract-builder/references/freeze-and-errata.md +81 -0
  57. package/skills/jarvis/SKILL.md +2 -0
  58. package/skills/release-archivist/SKILL.md +48 -13
  59. package/skills/session-handoff/SKILL.md +1 -0
  60. package/skills/spec-writer/SKILL.md +3 -0
  61. package/skills/spec-writer/references/facts-referencing.md +64 -0
  62. package/skills/test-strategy/SKILL.md +1 -1
  63. package/skills/workflow-start/SKILL.md +64 -5
  64. package/skills/workflow-start/references/routing-rules.md +4 -4
@@ -1,17 +1,61 @@
1
1
  // scripts/guard/checks/contract-fresh.mjs — check contract staleness via hash comparison
2
+ //
3
+ // v0.63.0(workflow-feedback 20260923-013114 S3):本维度新增挂载到 executing:closing。
4
+ // 动机:S3「DP-3 后 planning 制品冻结」的反查锚——原本 closing 侧反查依赖
5
+ // execution-plan-ready(validatePlan 比对 plan 内嵌的 artifacts_hash/contract_hash),
6
+ // 而该比对可被 `tf execution refresh-hash` 一键刷平(它只改 plan JSON)→ 把 gate-affecting
7
+ // 变更伪记为陈述性勘误后,closing 全维 PASS、wave receipt 亦不失效(wave_fingerprint
8
+ // 不含 artifacts_hash)→ **伪绿静默通过**。本维度直接比对 state.artifacts_hash 与制品实算值,
9
+ // refresh-hash 无法清屏(它不碰 state),故为独立锚。
10
+ //
11
+ // 注意(卡死面):本维度**无 legacy 豁免**(与既有挂载点一致)——存量 change 在 closing
12
+ // 若状态文件缺 artifacts_hash 会被拦。故失败信息必须给出可执行出路(tf state rebuild),
13
+ // 不得让门禁成为无出口的闭门(参照 arch_merge_skipped 的设计原则)。
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
2
16
  import { isContractFresh } from '../../lib/hash.mjs';
17
+ import { readState } from '../../lib/state-loader.mjs';
18
+
19
+ // v0.63.0(P4 评审 L3):裸 rebuild 会重算并覆盖 state.artifacts_hash——若发生在 DP-3 冻结之后,
20
+ // 它就成了与 refresh-hash 并列的「清屏」通道,且不留任何 errata 痕迹。故提示按时间点分流:
21
+ // 冻结前的改动可裸 rebuild;冻结后的改动必须走勘误登记/例外 2,不得用 rebuild 把差异抹平。
22
+ const REBUILD_HINT =
23
+ 'If the planning change predates DP-3 approval, re-capture the hash: `tf state rebuild <dir>`. '
24
+ + 'If it happened AFTER DP-3, the artifacts are FROZEN: record it in the contract `## Errata Register` '
25
+ + 'under the applicable exception (doc-only corrections), or take the gate-affecting path rebuild->revise '
26
+ + '(exception 2). Do NOT rebuild past a frozen change — that erases the diff without any errata trace.';
3
27
 
4
28
  /**
5
29
  * Compare stored artifacts_hash in .team-flow.yaml against current artifact hashes.
6
30
  * Returns { pass, failures[] }.
7
31
  */
8
32
  export function checkContractFresh(changeDir) {
9
- const fresh = isContractFresh(changeDir);
10
- if (fresh) {
33
+ if (isContractFresh(changeDir)) {
11
34
  return { pass: true, failures: [] };
12
35
  }
36
+
37
+ // 区分两类失败,给出各自的出路(否则关闭期死锁无指示)。
38
+ let stored = null;
39
+ try {
40
+ const state = readState(changeDir);
41
+ stored = state?.artifacts_hash ?? null;
42
+ } catch {
43
+ stored = null;
44
+ }
45
+ if (!stored || stored === 'null') {
46
+ return {
47
+ pass: false,
48
+ failures: [
49
+ 'execution-contract.md freshness cannot be judged: the state file carries no artifacts_hash '
50
+ + '(hand-written or truncated .team-flow.yaml). Re-capture it: `tf state rebuild <dir>`.',
51
+ ],
52
+ };
53
+ }
54
+
13
55
  return {
14
56
  pass: false,
15
- failures: ['execution-contract.md is stale: artifacts hash mismatch. Re-run contract-builder to regenerate.'],
57
+ failures: [
58
+ `execution-contract.md is stale: artifacts hash mismatch. ${REBUILD_HINT}`,
59
+ ],
16
60
  };
17
- }
61
+ }
@@ -0,0 +1,75 @@
1
+ // scripts/guard/checks/cross-change-consistency-light.mjs — 跨 change 冲突确定性初筛(§3.3)
2
+ // v0.64.0(A-14/S-07 重定义):guard runner 是 10s 内确定性 Node 函数,**不能调 LLM agent**。
3
+ // 本 runner 做机械初筛:其他在途 change(state ∈ executing/debugging/approved-for-build)
4
+ // 的 minimal api.md 端点路径 / architecture.md 聚合行 与本 change 的最小源求交——
5
+ // 交集非空 = 共享 surface 冲突。语义级冲突由 release-archivist 关门序列派发
6
+ // cross-change-consistency-checker agent 兜底(分工见 §3.3)。
7
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
8
+ import { join, basename, dirname } from 'node:path';
9
+ import { readState } from '../../lib/state-loader.mjs';
10
+ import { findProjectRoot } from '../../lib/surface-scan.mjs';
11
+
12
+ const IN_FLIGHT = new Set(['approved-for-build', 'executing', 'debugging']);
13
+
14
+ function extractApiPaths(file) {
15
+ if (!existsSync(file)) return new Set();
16
+ const text = readFileSync(file, 'utf-8');
17
+ // 端点表单元格里的路径 token(/api/xxx、/v1/xxx、{id} 形态)
18
+ const out = new Set();
19
+ for (const m of text.matchAll(/(\/[A-Za-z0-9_\-{}./]+)/g)) {
20
+ const p = m[1];
21
+ if (p.length > 1 && !p.endsWith('.md')) out.add(p.replace(/\/+$/, ''));
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function extractAggregateNames(file) {
27
+ if (!existsSync(file)) return new Set();
28
+ const text = readFileSync(file, 'utf-8');
29
+ const out = new Set();
30
+ for (const m of text.matchAll(/聚合[::\s*`]+[`*]*([A-Za-z][A-Za-z0-9_\-]*)/g)) out.add(m[1]);
31
+ return out;
32
+ }
33
+
34
+ export function checkCrossChangeConsistencyLight(changeDir) {
35
+ const state = readState(changeDir);
36
+ const projectRoot = findProjectRoot(changeDir);
37
+ const changesDir = join(projectRoot, 'changes');
38
+ if (!existsSync(changesDir)) return { pass: true, failures: [], reason: 'no changes/ dir' };
39
+
40
+ const selfName = state.change_name || basename(changeDir);
41
+ const selfApi = extractApiPaths(join(changeDir, 'architecture', 'api.md'));
42
+ const selfAgg = extractAggregateNames(join(changeDir, 'architecture', 'architecture.md'));
43
+
44
+ const conflicts = [];
45
+ for (const entry of readdirSync(changesDir, { withFileTypes: true })) {
46
+ if (!entry.isDirectory() || entry.name === selfName) continue;
47
+ const otherDir = join(changesDir, entry.name);
48
+ const otherStatePath = join(otherDir, '.team-flow.yaml');
49
+ if (!existsSync(otherStatePath)) continue;
50
+ const other = readState(otherDir);
51
+ if (!IN_FLIGHT.has(other.state)) continue;
52
+
53
+ const otherApi = extractApiPaths(join(otherDir, 'architecture', 'api.md'));
54
+ const otherAgg = extractAggregateNames(join(otherDir, 'architecture', 'architecture.md'));
55
+
56
+ const apiOverlap = [...selfApi].filter(p => otherApi.has(p));
57
+ const aggOverlap = [...selfAgg].filter(a => otherAgg.has(a));
58
+ if (apiOverlap.length || aggOverlap.length) {
59
+ conflicts.push(
60
+ `${entry.name}: api=[${apiOverlap.slice(0, 5).join(', ')}] aggregates=[${aggOverlap.slice(0, 5).join(', ')}]`
61
+ );
62
+ }
63
+ }
64
+
65
+ if (conflicts.length) {
66
+ return {
67
+ pass: false,
68
+ failures: [
69
+ `shared surface conflicts with in-flight change(s):\n ${conflicts.join('\n ')} `
70
+ + '— resolve ownership or stagger closing; semantic-level review via cross-change-consistency-checker agent',
71
+ ],
72
+ };
73
+ }
74
+ return { pass: true, failures: [] };
75
+ }
@@ -0,0 +1,52 @@
1
+ // scripts/guard/checks/direct-short-path.mjs — direct/lightweight 短路径 + 架构 surface 扫描
2
+ // v0.64.0(实施计划 §4,G1 加固):
3
+ // - 挂 quick/lightweight 的 exploring→approved-for-build / approved-for-build→executing / closing
4
+ // - G4 前提保障:无架构 surface(API/DB/聚合)→ 过(真琐碎零负担)
5
+ // - 命中架构 surface → FAIL 并给升级指引(禁止静默过);扫描失败/清单缺失一律 fail-closed
6
+ import { readState } from '../../lib/state-loader.mjs';
7
+ import { scanArchitectureSurface } from '../../lib/surface-scan.mjs';
8
+
9
+ const UPGRADE_HINT =
10
+ 'architecture surface (API/DB/aggregate) touched — upgrade to planned: '
11
+ + 'tf state upgrade <change-dir> planned (keeps code/test evidence, rolls back to approved-for-build, '
12
+ + 're-runs planned gates + light writeback sequence; downgrades rejected)';
13
+
14
+ export function checkDirectShortPath(changeDir) {
15
+ const state = readState(changeDir);
16
+ const scan = scanArchitectureSurface(changeDir, state);
17
+
18
+ if (scan.error === 'no-baseline') {
19
+ return {
20
+ pass: false,
21
+ failures: [
22
+ 'no scan baseline: base_sha missing and no origin/<default> to derive merge-base — '
23
+ + 'run inside a git repo with at least one commit (tf state init stamps base_sha), fail-closed',
24
+ ],
25
+ };
26
+ }
27
+ if (scan.error === 'diff-failed') {
28
+ return {
29
+ pass: false,
30
+ failures: [`git diff against base failed (base=${scan.base ?? 'unknown'}) — cannot verify architecture surface, fail-closed`],
31
+ };
32
+ }
33
+ if (scan.error === 'aggregate-list-missing') {
34
+ return {
35
+ pass: false,
36
+ failures: [
37
+ 'aggregate list missing: create .team-flow/aggregate-dirs.txt (one dir prefix per line; '
38
+ + 'empty file = explicitly no aggregates) or run workflow-bootstrap — fail-closed, aggregate surface is blind otherwise',
39
+ ],
40
+ };
41
+ }
42
+
43
+ if (scan.architectureSurface.length > 0) {
44
+ return {
45
+ pass: false,
46
+ failures: [
47
+ `${UPGRADE_HINT}\n touched: ${scan.architectureSurface.slice(0, 10).join(', ')}`,
48
+ ],
49
+ };
50
+ }
51
+ return { pass: true, failures: [] };
52
+ }
@@ -0,0 +1,30 @@
1
+ // scripts/guard/checks/direct-test-result.mjs — direct/lightweight 验证证据(§4.5 注记③)
2
+ // v0.64.0:guard 只读 tf test record 程序化证据,绝不在 guard 进程内现场跑测试
3
+ // (cmd-state spawnSync 10s 总超时会杀掉 guard,A-19)。证据机制与 tests-passing 同源。
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { readState } from '../../lib/state-loader.mjs';
7
+ import { parseStructuredTestResult } from './tests-passing.mjs';
8
+
9
+ const RECORD_HINT = 'run your verification command, then record it: tf test record <change-dir> --from <runner-output>';
10
+
11
+ export function checkDirectTestResult(changeDir) {
12
+ const state = readState(changeDir);
13
+ const parsed = parseStructuredTestResult(state.test_result);
14
+ if (!parsed || parsed.verdict !== 'pass') {
15
+ return { pass: false, failures: [`no passing programmatic test evidence recorded — ${RECORD_HINT}`] };
16
+ }
17
+ if (parsed.total == null || parsed.total <= 0) {
18
+ return { pass: false, failures: [`recorded evidence has total=0 (empty-run rejected) — ${RECORD_HINT}`] };
19
+ }
20
+ if (!state.test_evidence_path) {
21
+ return { pass: false, failures: [`test_evidence_path missing — ${RECORD_HINT}`] };
22
+ }
23
+ const evidenceAbs = path.isAbsolute(state.test_evidence_path)
24
+ ? state.test_evidence_path
25
+ : path.join(changeDir, state.test_evidence_path);
26
+ if (!fs.existsSync(evidenceAbs)) {
27
+ return { pass: false, failures: [`evidence file not found: ${state.test_evidence_path} — ${RECORD_HINT}`] };
28
+ }
29
+ return { pass: true, failures: [] };
30
+ }
@@ -7,7 +7,7 @@ import { readState } from '../../lib/state-loader.mjs';
7
7
  * A non-empty DP-4 field is not sufficient: it must name the current plan
8
8
  * revision, whose hashes, workflow, mode, and state summary remain current.
9
9
  */
10
- export function checkExecutionPlanReady(changeDir) {
10
+ export function checkExecutionPlanReady(changeDir, ctx = {}) {
11
11
  const plan = readPlan(changeDir);
12
12
  if (!plan) {
13
13
  return {
@@ -21,6 +21,12 @@ export function checkExecutionPlanReady(changeDir) {
21
21
  return { pass: false, failures: validation.failures };
22
22
  }
23
23
 
24
+ // v0.64.0(§4.5 注记⑤,R2 兼容性修复):planned 无 DP-4——豁免 DP-4 revision 精确引用,
25
+ // 判据 = plan 存在 + validatePlan 通过(内嵌 hash 与制品一致)。
26
+ if (ctx.variant === 'planned') {
27
+ return { pass: true, failures: [], reason: 'planned variant: DP-4 reference waived; plan file + hash validation suffice' };
28
+ }
29
+
24
30
  const state = readState(changeDir);
25
31
  const expectedRevision = `plan revision ${plan.revision}`;
26
32
  const decision = typeof state.dp_4_result === 'string' ? state.dp_4_result : '';
@@ -0,0 +1,28 @@
1
+ // scripts/guard/checks/execution-reviews-passed-light.mjs — planned 一次最终审查(§4.5)
2
+ // v0.64.0(A-05①):planned 无波次无逐波 receipt——判据 = 关门前存在非空(≥5 非空行)
3
+ // 最终审查记录 final-review.md;内容最低契约(B-01)防空文件桩。
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const MIN_LINES = 5;
8
+
9
+ export function checkExecutionReviewsPassedLight(changeDir) {
10
+ const p = join(changeDir, 'final-review.md');
11
+ if (!existsSync(p)) {
12
+ return {
13
+ pass: false,
14
+ failures: [
15
+ 'final-review.md missing — planned path requires ONE final review before closing '
16
+ + '(record verdict, scope, and findings; ≥5 lines)',
17
+ ],
18
+ };
19
+ }
20
+ const lines = readFileSync(p, 'utf-8').split('\n').filter(l => l.trim()).length;
21
+ if (lines < MIN_LINES) {
22
+ return {
23
+ pass: false,
24
+ failures: [`final-review.md too thin (${lines} non-empty lines, need ≥${MIN_LINES}) — record verdict + scope + findings`],
25
+ };
26
+ }
27
+ return { pass: true, failures: [] };
28
+ }
@@ -0,0 +1,175 @@
1
+ // scripts/guard/checks/gates-probed.mjs — bridging dry-run evidence gate
2
+ //
3
+ // v0.63.0(workflow-feedback 20260923-013114 S2):
4
+ // full workflow 的 bridging→approved-for-build 新增本维度——契约产出的 G 类闸门
5
+ // 必须已在**主工作区当前态**跑过 dry-run(预期 FAIL = RED 基线),原始输出留档。
6
+ // 动机:`design.md` R-7「闸门命令已实测可跑」长期只是**文字自证**;实测中 G1 格式约束、
7
+ // grep shim、dist 误入扫描、desc 文案 vs G2 互斥均在施工中才暴露。
8
+ //
9
+ // **存在性强制(Critical 处置,不得退回内容型豁免)**:
10
+ // 契约缺 `## Gate Registry` 段 / 表解析失败 → **FAIL**,不得落入 N/A 放行。
11
+ // 依据:v0.13 RC-1 已删除「以 contract 内容为键」的豁免——test-gate-exemptions.mjs
12
+ // :7-10 明文记载「无法区分真存量与漏生成,导致零测试通过全部门禁」;既有先例
13
+ // `contractDeclaresTestMatrix` 走的是段存在性硬要求。本 check 复刻该先例。
14
+ //
15
+ // 与 test-matrix-ready(:49 入口门禁)同构:legacy 豁免 + 显式 skip 附理由。
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { readState } from '../../lib/state-loader.mjs';
19
+ import { computeContractHash } from '../../lib/hash.mjs';
20
+ import { isLegacyChange } from './test-gate-exemptions.mjs';
21
+
22
+ export const EVIDENCE_REL = path.join('.superpowers', 'test-evidence', 'bridging-gates-red.txt');
23
+ const REGISTRY_HEADING = '## Gate Registry';
24
+ const RED_BASELINE_LINE = 'EXPECTED: FAIL (RED baseline)';
25
+ const SKIP_REASON_HINT =
26
+ 'gates_probed_skipped=true requires gates_probed_skip_reason — record it: '
27
+ + "tf state set <dir> gates_probed_skip_reason '<why this change has no gate dry-run>'";
28
+
29
+ /** 提取 '## Gate Registry' 段正文(到下一个同级标题或文件尾)。 */
30
+ export function extractRegistrySection(contractText) {
31
+ const lines = contractText.split(/\r?\n/);
32
+ const start = lines.findIndex((l) => l.trim() === REGISTRY_HEADING);
33
+ if (start === -1) return null;
34
+ const body = [];
35
+ for (let i = start + 1; i < lines.length; i += 1) {
36
+ if (/^##\s/.test(lines[i])) break;
37
+ body.push(lines[i]);
38
+ }
39
+ return body.join('\n');
40
+ }
41
+
42
+ /**
43
+ * 解析 Gate Registry 表格的 id 列。
44
+ * 表头固定 `| id | phase | command | expected |`;id 形如 G-1。
45
+ * @returns {string[]} id 列表(去重,出现顺序)
46
+ */
47
+ export function parseGateIds(sectionBody) {
48
+ const ids = [];
49
+ for (const line of sectionBody.split(/\r?\n/)) {
50
+ if (!line.trim().startsWith('|')) continue;
51
+ const cells = line.split('|').map((c) => c.trim());
52
+ // cells[0] === ''(前导竖线),故首列在 index 1
53
+ const first = cells[1] || '';
54
+ if (!first || /^-+$/.test(first) || first.toLowerCase() === 'id') continue;
55
+ if (!ids.includes(first)) ids.push(first);
56
+ }
57
+ return ids;
58
+ }
59
+
60
+ /** 段内显式声明零闸门 + 理由(形如 `N/A: <理由>`)。 */
61
+ export function declaredNotApplicable(sectionBody) {
62
+ const m = sectionBody.match(/(?:^|\n)\s*(?:N\/A|NA)\s*[::]\s*(.+)/i);
63
+ return m ? m[1].trim() : null;
64
+ }
65
+
66
+ /**
67
+ * Returns { pass, failures[], reason? }.
68
+ */
69
+ export function checkGatesProbed(changeDir) {
70
+ const state = readState(changeDir);
71
+
72
+ // Exemption 1:legacy change(v0.32.0 之前初始化,无 schema_version 打戳)。
73
+ if (isLegacyChange(state)) {
74
+ return { pass: true, failures: [], reason: 'legacy change — initialized before v0.32.0' };
75
+ }
76
+
77
+ // Exemption 2:显式 skip(必须附理由)。
78
+ if (state.gates_probed_skipped === 'true') {
79
+ const reason = state.gates_probed_skip_reason;
80
+ if (typeof reason !== 'string' || reason.trim().length === 0) {
81
+ return { pass: false, failures: [SKIP_REASON_HINT] };
82
+ }
83
+ return { pass: true, failures: [], reason: `explicitly skipped: ${reason}` };
84
+ }
85
+
86
+ // 段存在性硬要求(fail-closed):缺段 / 解析失败一律 FAIL,不得落 N/A。
87
+ const contractPath = path.join(changeDir, 'execution-contract.md');
88
+ if (!fs.existsSync(contractPath)) {
89
+ return { pass: false, failures: ['execution-contract.md is missing — run contract-builder first'] };
90
+ }
91
+ const contractText = fs.readFileSync(contractPath, 'utf-8');
92
+ const section = extractRegistrySection(contractText);
93
+ if (section === null) {
94
+ return {
95
+ pass: false,
96
+ failures: [
97
+ `contract does not declare ${REGISTRY_HEADING} — return to bridging so contract-builder generates it.`,
98
+ 'A missing section is NOT a "no gates" declaration: absence of the registry cannot be distinguished '
99
+ + 'from an omitted registry (v0.13 RC-1 content-key exemption was removed for exactly this reason).',
100
+ 'If this change genuinely has no gates, write the section with an explicit "N/A: <reason>" line.',
101
+ ],
102
+ };
103
+ }
104
+ const gateIds = parseGateIds(section);
105
+
106
+ // N/A:段存在 + 显式声明零闸门 + 理由(state 键为备用通道)。
107
+ if (gateIds.length === 0) {
108
+ const naReason = declaredNotApplicable(section);
109
+ if (naReason) {
110
+ return { pass: true, failures: [], reason: `not applicable — ${naReason}` };
111
+ }
112
+ if (state.gates_probed_na === 'true') {
113
+ return { pass: true, failures: [], reason: 'not applicable — gates_probed_na set in state' };
114
+ }
115
+ return {
116
+ pass: false,
117
+ failures: [
118
+ `${REGISTRY_HEADING} present but declares no gate id and no "N/A: <reason>" line.`,
119
+ 'Either list the gates (| id | phase | command | expected |) or state "N/A: <reason>".',
120
+ ],
121
+ };
122
+ }
123
+
124
+ // 正常路径:evidence 文件 + 首行 RED 基线 + hash 值新鲜 + id 全覆盖。
125
+ const evidencePath = path.join(changeDir, EVIDENCE_REL);
126
+ if (!fs.existsSync(evidencePath)) {
127
+ return {
128
+ pass: false,
129
+ failures: [
130
+ `${EVIDENCE_REL} is missing — run the bridging gate dry-run (expect FAIL = RED baseline) `
131
+ + 'and persist the raw output there.',
132
+ 'Required first line: ' + RED_BASELINE_LINE,
133
+ ],
134
+ };
135
+ }
136
+ const evidence = fs.readFileSync(evidencePath, 'utf-8');
137
+ const failures = [];
138
+ const evidenceLines = evidence.split(/\r?\n/);
139
+ const firstNonEmpty = evidenceLines.find((l) => l.trim().length > 0) || '';
140
+ if (firstNonEmpty.trim() !== RED_BASELINE_LINE) {
141
+ failures.push(
142
+ `evidence first line must be exactly "${RED_BASELINE_LINE}", got: "${firstNonEmpty.trim().slice(0, 80)}". `
143
+ + 'A gate that already PASSES means the baseline was not RED — record the expected-FAIL run instead.',
144
+ );
145
+ }
146
+
147
+ // 新鲜度 = hash 值比对(非 mtime):
148
+ // refresh-hash / state rebuild 均不重写契约文件,故 mtime 判据只会在「契约字节等价重生成」
149
+ // 或 git 恢复场景下误报;hash 值比对天然免疫。
150
+ const contractHash = computeContractHash(changeDir);
151
+ const hashMatch = evidence.match(/CONTRACT_HASH:\s*(sha256:[0-9a-f]{64})/);
152
+ if (!hashMatch) {
153
+ failures.push(
154
+ 'evidence must carry a "CONTRACT_HASH: sha256:<64 hex>" line so freshness is judged by content, not mtime.',
155
+ );
156
+ } else if (contractHash && hashMatch[1] !== contractHash) {
157
+ failures.push(
158
+ `evidence is stale: CONTRACT_HASH ${hashMatch[1].slice(0, 20)}… does not match current contract `
159
+ + `${String(contractHash).slice(0, 20)}… — re-run the dry-run against the current contract.`,
160
+ );
161
+ }
162
+
163
+ const missingIds = gateIds.filter((id) => {
164
+ const esc = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
165
+ return !new RegExp(`(^|[^\\w-])${esc}([^\\w-]|$)`).test(evidence);
166
+ });
167
+ if (missingIds.length > 0) {
168
+ failures.push(
169
+ `evidence does not cover declared gate id(s): ${missingIds.join(', ')} — every id in ${REGISTRY_HEADING} needs a dry-run record.`,
170
+ );
171
+ }
172
+
173
+ if (failures.length > 0) return { pass: false, failures };
174
+ return { pass: true, failures: [] };
175
+ }
@@ -0,0 +1,27 @@
1
+ // scripts/guard/checks/lightweight-completion-evidence.mjs — lightweight 档完成证据(§4.5)
2
+ // v0.64.0:类 direct-test-result——证据文件须存在且非空(独立于 test_result 解析,
3
+ // 兜住「test_result 被清但文件还在」与「文件被删但记录还在」两种漂移)。
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { readState } from '../../lib/state-loader.mjs';
7
+
8
+ export function checkLightweightCompletionEvidence(changeDir) {
9
+ const state = readState(changeDir);
10
+ if (!state.test_evidence_path) {
11
+ return {
12
+ pass: false,
13
+ failures: ['completion evidence missing: test_evidence_path is null — record verification via tf test record before closing'],
14
+ };
15
+ }
16
+ const abs = path.isAbsolute(state.test_evidence_path)
17
+ ? state.test_evidence_path
18
+ : path.join(changeDir, state.test_evidence_path);
19
+ if (!fs.existsSync(abs)) {
20
+ return { pass: false, failures: [`completion evidence file not found: ${state.test_evidence_path}`] };
21
+ }
22
+ const content = fs.readFileSync(abs, 'utf-8').trim();
23
+ if (!content) {
24
+ return { pass: false, failures: [`completion evidence file is empty: ${state.test_evidence_path}`] };
25
+ }
26
+ return { pass: true, failures: [] };
27
+ }
@@ -34,11 +34,35 @@ function hasDeltaSpecs(changeDir) {
34
34
  * Passes when either spec_merged is recorded, or there are no delta specs to merge.
35
35
  * Blocks `executing → closing` when delta specs exist but spec-merger hasn't run.
36
36
  */
37
- export function checkSpecsMerged(changeDir) {
37
+ export function checkSpecsMerged(changeDir, ctx = {}) {
38
38
  const state = readState(changeDir);
39
39
  if (state.spec_merged === true || state.spec_merged === 'true') {
40
40
  return { pass: true, failures: [] };
41
41
  }
42
+ // v0.64.0(§4.5 注记⑦,B-02):planned 轻语义——无 specs/ 目录即过;
43
+ // 有 specs/ 但无 delta 头 = 手写完整形态 specs 永不合并 → FAIL(补 delta 头或移除)。
44
+ if (ctx.variant === 'planned') {
45
+ const specsDir = join(changeDir, 'specs');
46
+ if (!existsSync(specsDir)) {
47
+ return { pass: true, failures: [], reason: 'planned: no specs/ dir — merge N/A' };
48
+ }
49
+ if (!hasDeltaSpecs(changeDir)) {
50
+ return {
51
+ pass: false,
52
+ failures: [
53
+ 'specs/ exists but no ## ADDED/MODIFIED/REMOVED/RENAMED Requirements delta headers — '
54
+ + 'full-form specs would never merge; add delta headers (then spec-merger) or remove specs/',
55
+ ],
56
+ };
57
+ }
58
+ // 有 delta 头但未合并 → 落到下方统一失败
59
+ return {
60
+ pass: false,
61
+ failures: [
62
+ 'Delta specs exist in specs/ but spec-merger has not run (spec_merged not recorded). Run spec-merger to merge ADDED/MODIFIED/REMOVED/RENAMED requirements before closing.',
63
+ ],
64
+ };
65
+ }
42
66
  if (!hasDeltaSpecs(changeDir)) {
43
67
  return { pass: true, failures: [] };
44
68
  }
@@ -19,7 +19,7 @@ import {
19
19
  /**
20
20
  * Returns { pass, failures[], reason? }.
21
21
  */
22
- export function checkTestMatrixComplete(changeDir) {
22
+ export function checkTestMatrixComplete(changeDir, ctx = {}) {
23
23
  const state = readState(changeDir);
24
24
 
25
25
  // Step 1 — legacy:v0.32.0 之前初始化的 change(无 schema_version 打戳)保留旧行为。
@@ -40,6 +40,32 @@ export function checkTestMatrixComplete(changeDir) {
40
40
  return { pass: true, failures: [], reason: `explicitly skipped: ${state.test_matrix_skip_reason}` };
41
41
  }
42
42
 
43
+ // v0.64.0(§4.5 注记②,D4 无契约轻量判据):direct/planned/quick/lightweight
44
+ // 无契约 → 跳过 Step 3 契约声明检查;矩阵存在非空 +(若已记录)hash 一致即可。
45
+ const isLight = ctx.variant === 'direct' || ctx.variant === 'planned'
46
+ || ctx.workflow === 'quick' || ctx.workflow === 'lightweight';
47
+ if (isLight) {
48
+ const matrix = readTestMatrixFile(changeDir);
49
+ if (!matrix.exists || matrix.empty) {
50
+ return {
51
+ pass: false,
52
+ failures: [
53
+ !matrix.exists
54
+ ? 'test-matrix.md missing — light path requires the matrix file itself (no contract)'
55
+ : 'test-matrix.md is empty — must contain at least a Summary section and Cases table',
56
+ 'or skip explicitly: tf state set <dir> test_matrix_skipped true + test_matrix_skip_reason "<reason>"',
57
+ ],
58
+ };
59
+ }
60
+ if (state.test_matrix_hash) {
61
+ const computedLight = computeTestMatrixHash(changeDir);
62
+ if (computedLight !== state.test_matrix_hash) {
63
+ return { pass: false, failures: ['test-matrix.md changed since recorded hash — run tf state rebuild'] };
64
+ }
65
+ }
66
+ return { pass: true, failures: [] };
67
+ }
68
+
43
69
  // Step 3 — 非存量 change 的契约必须声明矩阵(v0.13:内容型豁免删除)。
44
70
  const contract = contractDeclaresTestMatrix(changeDir);
45
71
  if (!contract.exists) {
@@ -17,7 +17,7 @@ import {
17
17
  /**
18
18
  * Returns { pass, failures[], reason? }.
19
19
  */
20
- export function checkTestMatrixReady(changeDir) {
20
+ export function checkTestMatrixReady(changeDir, ctx = {}) {
21
21
  const state = readState(changeDir);
22
22
 
23
23
  // Exemption 1:legacy change(v0.32.0 之前初始化,无 schema_version 打戳)。
@@ -25,6 +25,33 @@ export function checkTestMatrixReady(changeDir) {
25
25
  return { pass: true, failures: [], reason: 'legacy change — initialized before v0.32.0' };
26
26
  }
27
27
 
28
+ // v0.64.0(§4.5 注记②,D4 无契约轻量判据):direct/planned/quick/lightweight
29
+ // 无 execution-contract——判据降为「test-matrix.md 存在非空 OR 显式 skip 附理由」,
30
+ // 不要求契约声明段。skip 仍须附理由(拒 state set 自清为常规通过路径)。
31
+ const isLight = ctx.variant === 'direct' || ctx.variant === 'planned'
32
+ || ctx.workflow === 'quick' || ctx.workflow === 'lightweight';
33
+ if (isLight) {
34
+ if (state.test_matrix_skipped === 'true') {
35
+ if (skipMissingReason(state)) {
36
+ return { pass: false, failures: [SKIP_REASON_HINT] };
37
+ }
38
+ return { pass: true, failures: [], reason: `explicitly skipped: ${state.test_matrix_skip_reason}` };
39
+ }
40
+ const matrix = readTestMatrixFile(changeDir);
41
+ if (matrix.exists && !matrix.empty) {
42
+ return { pass: true, failures: [] };
43
+ }
44
+ return {
45
+ pass: false,
46
+ failures: [
47
+ matrix.exists
48
+ ? 'test-matrix.md is empty — add a Summary + Cases table (light path has no contract section)'
49
+ : 'test-matrix.md missing — create it (light path: matrix file alone is sufficient, no contract needed)',
50
+ 'or skip explicitly: tf state set <dir> test_matrix_skipped true + test_matrix_skip_reason "<reason>"',
51
+ ],
52
+ };
53
+ }
54
+
28
55
  // Exemption 2:显式 skip(必须附理由)。
29
56
  if (state.test_matrix_skipped === 'true') {
30
57
  // v2.1 §6.3 skip 禁令:glaf4-delegation 模式禁止 skip(唯一例外 TEST_BOOTSTRAP)
@@ -0,0 +1,36 @@
1
+ // scripts/guard/checks/test-merged-light.mjs — 测试台账轻回写(§4,横展发现的同型全局态)
2
+ // v0.64.0:无测试触碰 → 过;有 → docs/test-ledger/** 须含归因 change:<name>
3
+ //(写入走 tf test-merge --light,同 sink 同 rewriteIndex 单写入口)。
4
+ import { readState } from '../../lib/state-loader.mjs';
5
+ import { scanArchitectureSurface } from '../../lib/surface-scan.mjs';
6
+ import { filesContainDir } from './_fs-utils.mjs';
7
+ import { join } from 'node:path';
8
+
9
+ export function checkTestMergedLight(changeDir) {
10
+ const state = readState(changeDir);
11
+ const changeName = state.change_name || changeDir.split('/').filter(Boolean).pop();
12
+ const scan = scanArchitectureSurface(changeDir, state);
13
+
14
+ if (scan.error === 'no-baseline' || scan.error === 'diff-failed') {
15
+ return { pass: false, failures: [`surface scan failed (${scan.error}) — cannot determine touched tests, fail-closed`] };
16
+ }
17
+ if (scan.error === 'aggregate-list-missing') {
18
+ // 测试判定不依赖聚合清单——降级为仅用文件清单分类(清单缺失不阻塞本维度)
19
+ // 但 scanArchitectureSurface 在清单缺失时不返回 files……此时保守 FAIL:
20
+ return { pass: false, failures: ['aggregate list missing (.team-flow/aggregate-dirs.txt) — shared scan precondition unmet, fail-closed'] };
21
+ }
22
+
23
+ if (scan.testFiles.length === 0) {
24
+ return { pass: true, failures: [], reason: 'no test files touched — test ledger writeback N/A' };
25
+ }
26
+
27
+ const ledger = join(scan.projectRoot, 'docs', 'test-ledger');
28
+ const anchor = `change:${changeName}`;
29
+ if (filesContainDir(ledger, anchor) || filesContainDir(ledger, changeName)) {
30
+ return { pass: true, failures: [] };
31
+ }
32
+ return {
33
+ pass: false,
34
+ failures: [`tests touched but docs/test-ledger/** has no '${anchor}' entry — run: tf test-merge --light <change-dir>`],
35
+ };
36
+ }