@michaelxwb/code-flow 0.5.1 → 0.5.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.
@@ -29,6 +29,11 @@ from cf_core import (
29
29
 
30
30
  TOTAL_BUDGET_SECONDS = 30.0
31
31
  _BRACE_RE = re.compile(r"\{([^{}]+)\}")
32
+ _TASK_SECTION_RE = re.compile(
33
+ r"(?ms)^## (TASK-\d+):.*?(?=^## TASK-\d+:|\Z)"
34
+ )
35
+ _SCENARIO_RE = re.compile(r"\b[SEB]-\d+\b")
36
+ _UNVERIFIED_RE = re.compile(r"\b(?:planned|pending|tbd)\b", re.IGNORECASE)
32
37
 
33
38
 
34
39
  def expand_braces(pattern: str) -> list:
@@ -83,6 +88,79 @@ def session_edited_files(project_root: str, sid: str) -> list:
83
88
  return files
84
89
 
85
90
 
91
+ def _is_active_task_file(rel_path: str) -> bool:
92
+ path = normalize_path(rel_path)
93
+ if not path.startswith(".code-flow/tasks/") or "/archived/" in path:
94
+ return False
95
+ return path.endswith(".md") and not path.endswith((".design.md", ".prd.md"))
96
+
97
+
98
+ def _subsection(section: str, heading: str) -> str:
99
+ match = re.search(
100
+ rf"(?ms)^### {re.escape(heading)}\s*$\n(.*?)(?=^### |^## |\Z)",
101
+ section,
102
+ )
103
+ return match.group(1) if match else ""
104
+
105
+
106
+ def _acceptance_gap(task_id: str, section: str, coverage: str) -> str:
107
+ status = re.search(r"(?m)^- \*\*Status\*\*: ([^\n]+)", section)
108
+ if not status or status.group(1).strip() != "done":
109
+ return ""
110
+ refs_match = re.search(r"(?m)^- \*\*Acceptance-Refs\*\*: ([^\n]+)", section)
111
+ if not refs_match:
112
+ return f"{task_id} 缺少 Acceptance-Refs"
113
+ refs = refs_match.group(1).strip()
114
+ if refs.upper().startswith("N/A"):
115
+ return ""
116
+ scenarios = sorted(set(_SCENARIO_RE.findall(refs)))
117
+ if not scenarios:
118
+ return f"{task_id} Acceptance-Refs 未引用 S/E/B 场景"
119
+ contract = _subsection(section, "Acceptance Contract")
120
+ evidence = _subsection(section, "Acceptance Evidence")
121
+ if not contract or not evidence:
122
+ return f"{task_id} 缺少 Acceptance Contract 或 Acceptance Evidence"
123
+ if _UNVERIFIED_RE.search(contract) or _UNVERIFIED_RE.search(evidence):
124
+ return f"{task_id} 验收契约仍有 planned/pending/TBD"
125
+ for scenario in scenarios:
126
+ contract_ok = any(scenario in line and "verified" in line.lower()
127
+ for line in contract.splitlines())
128
+ evidence_ok = any(scenario in line and "verified" in line.lower()
129
+ for line in evidence.splitlines())
130
+ coverage_ok = any(scenario in line and "verified" in line.lower()
131
+ for line in coverage.splitlines())
132
+ if not contract_ok or not evidence_ok or not coverage_ok:
133
+ return f"{task_id} 的 {scenario} 未在覆盖表、契约和证据中全部 verified"
134
+ return ""
135
+
136
+
137
+ def task_acceptance_failures(project_root: str, files: list) -> list:
138
+ """Check new-format task files; legacy tasks without coverage are ignored."""
139
+ failures = []
140
+ for rel_path in files:
141
+ if not _is_active_task_file(rel_path):
142
+ continue
143
+ path = os.path.join(project_root, normalize_path(rel_path))
144
+ try:
145
+ with open(path, "r", encoding="utf-8") as f:
146
+ text = f.read()
147
+ except OSError as exc:
148
+ _log(f"cf_stop_hook task read failed: {exc}")
149
+ continue
150
+ if "## Acceptance Coverage" not in text:
151
+ continue
152
+ coverage = text.split("## Acceptance Coverage", 1)[1].split("## TASK-", 1)[0]
153
+ for match in _TASK_SECTION_RE.finditer(text):
154
+ detail = _acceptance_gap(match.group(1), match.group(0), coverage)
155
+ if detail:
156
+ failures.append({
157
+ "name": "任务验收契约",
158
+ "on_fail": "补齐设计场景的测试映射与执行证据后再完成任务",
159
+ "detail": detail,
160
+ })
161
+ return failures
162
+
163
+
86
164
  def run_validators(
87
165
  project_root: str, validators: list, files: list, sid: str,
88
166
  total_budget: float = TOTAL_BUDGET_SECONDS,
@@ -144,7 +222,7 @@ def run_validators(
144
222
 
145
223
 
146
224
  def _reason_text(failures: list, truncated: bool) -> str:
147
- lines = ["收尾校验未通过(cf-stop,FEAT-03):"]
225
+ lines = ["收尾校验未通过(cf-stop):"]
148
226
  for item in failures:
149
227
  lines.append(f"✗ {item['name']}:{item['on_fail']}")
150
228
  if item["detail"]:
@@ -174,12 +252,14 @@ def main() -> None:
174
252
  files = session_edited_files(project_root, sid)
175
253
  if not files:
176
254
  return
255
+ acceptance_failures = task_acceptance_failures(project_root, files)
177
256
  validators = load_validators(project_root)
178
- if not validators:
179
- return # E-05: 无 validation.yml 静默
180
- failures, truncated = run_validators(project_root, validators, files, sid)
257
+ failures, truncated = run_validators(
258
+ project_root, validators, files, sid
259
+ ) if validators else ([], False)
260
+ failures = acceptance_failures + failures
181
261
  if not failures:
182
- return # 全过静默(S-05)
262
+ return # 全过或无 validation.yml 且无验收缺口时静默
183
263
  payload = {"decision": "block", "reason": _reason_text(failures, truncated)}
184
264
  sys.stdout.write(json.dumps(payload, ensure_ascii=False))
185
265
  except Exception as exc:
@@ -90,19 +90,19 @@
90
90
 
91
91
  ### 2.4 验收条件 [必填]
92
92
 
93
- > 交互层面的可验证场景,写到可转 e2e / 组件交互测试断言的粒度(明确操作与可观测 UI 结果)。场景 ID 即测试用例来源。
93
+ > 交互层面的可验证场景,写到可转 E2E / 组件交互测试断言的粒度(明确操作与可观测 UI 结果)。场景 ID 即测试用例来源。测试层级只能填 `unit` / `integration` / `E2E` / `manual`;跨路由、服务请求、状态更新和最终 UI 的用户流程必须标为 `E2E`。关键真实边界列出不得 mock 的浏览器、路由、状态或服务边界。
94
94
 
95
95
  **正常场景**
96
96
 
97
- | 场景ID | 功能ID | 优先级 | 操作步骤 | 预期 UI 结果 |
98
- |--------|--------|--------|---------|-------------|
99
- | S-01 | FEAT-01 | P0 | 1. <操作> | <可见结果> |
97
+ | 场景ID | 功能ID | 优先级 | 测试层级 | 关键真实边界 | 操作步骤 | 预期 UI 结果 |
98
+ |--------|--------|--------|---------|-------------|---------|-------------|
99
+ | S-01 | FEAT-01 | P0 | E2E | Browser → Router → Service → UI | 1. <操作> | <可见结果> |
100
100
 
101
101
  **异常场景**
102
102
 
103
- | 场景ID | 功能ID | 触发条件 | UI 表现 |
104
- |--------|--------|---------|---------|
105
- | E-01 | FEAT-01 | (接口失败 / 空数据 / 校验不通过) | (错误提示 / 空态 / 字段定位) |
103
+ | 场景ID | 功能ID | 测试层级 | 关键真实边界 | 触发条件 | UI 表现 |
104
+ |--------|--------|---------|-------------|---------|---------|
105
+ | E-01 | FEAT-01 | integration | Service → UI | (接口失败 / 空数据 / 校验不通过) | (错误提示 / 空态 / 字段定位) |
106
106
 
107
107
  **非功能指标** [按需]
108
108
 
@@ -227,9 +227,9 @@
227
227
 
228
228
  ## 4. 风险与依赖 [按需]
229
229
 
230
- | 风险ID | 描述 | 影响 | 应对 |
231
- |--------|------|------|------|
232
- | | | | |
230
+ | 风险ID | 描述 | 影响 | 应对 | 验证场景 |
231
+ |--------|------|------|------|---------|
232
+ | | | | | S-/E-/B- 或手动验证理由 |
233
233
 
234
234
  ---
235
235
 
@@ -133,32 +133,34 @@
133
133
 
134
134
  #### 2.5.1 业务规则与约束
135
135
 
136
- | ID | 类型 | 描述 |
137
- |----|------|------|
138
- | RULE-01 | 业务规则 | |
139
- | RULE-02 | 系统约束 | 单次批量导入上限≤10000条 |
136
+ | ID | 类型 | 描述 | 验证场景 |
137
+ |----|------|------|---------|
138
+ | RULE-01 | 业务规则 | | S-01 |
139
+ | RULE-02 | 系统约束 | 单次批量导入上限≤10000条 | B-01 |
140
140
 
141
141
  #### 2.5.2 功能验收场景
142
142
 
143
- > 场景写到可转自动化测试断言的粒度(明确前置、输入、可观测输出)。场景 ID(S-/E-/B-)即测试用例来源,§6 追溯矩阵的 TC-XXX 直接引用这些场景 ID。
143
+ > 场景写到可转自动化测试断言的粒度(明确前置、输入、可观测输出)。场景 ID(S-/E-/B-)即测试用例来源,§6 追溯矩阵直接引用这些场景 ID。
144
+ >
145
+ > **测试层级**只能填 `unit` / `integration` / `E2E` / `manual`。跨 API、存储、运行时生成、渲染或用户可见结果等多个边界的场景必须标为 `E2E`;`manual` 仅限无法自动化的外部条件,并记录原因。**关键真实边界**列出测试中不得 mock 的组件或最终可观测面,编码阶段不得自行降级。
144
146
 
145
147
  **正常场景**
146
148
 
147
- | 场景ID | 功能ID | 优先级 | 前置条件 | 操作步骤 | 预期结果 |
148
- |--------|--------|--------|---------|---------|---------|
149
- | S-01 | FEAT-01 | P0 | | | |
149
+ | 场景ID | 功能ID | 优先级 | 测试层级 | 关键真实边界 | 前置条件 | 操作步骤 | 预期结果 |
150
+ |--------|--------|--------|---------|-------------|---------|---------|---------|
151
+ | S-01 | FEAT-01 | P0 | E2E | API → Store → 最终输出 | | | |
150
152
 
151
153
  **异常场景**
152
154
 
153
- | 场景ID | 功能ID | 触发条件 | 系统行为 | 用户感知 |
154
- |--------|--------|---------|---------|---------|
155
- | E-01 | FEAT-01 | | | |
155
+ | 场景ID | 功能ID | 测试层级 | 关键真实边界 | 触发条件 | 系统行为 | 用户感知 |
156
+ |--------|--------|---------|-------------|---------|---------|---------|
157
+ | E-01 | FEAT-01 | integration | Service → Store | | | |
156
158
 
157
159
  **边界场景** [按需]
158
160
 
159
- | 场景ID | 字段/条件 | 边界值 | 预期行为 |
160
- |--------|----------|--------|---------|
161
- | B-01 | | | |
161
+ | 场景ID | 测试层级 | 关键真实边界 | 字段/条件 | 边界值 | 预期行为 |
162
+ |--------|---------|-------------|----------|--------|---------|
163
+ | B-01 | unit | 校验函数 | | | |
162
164
 
163
165
  #### 2.5.3 非功能指标 [按需]
164
166
 
@@ -386,9 +388,9 @@ flowchart TD
386
388
 
387
389
  **风险 / 失效应对**(默认轻量表)
388
390
 
389
- | 风险ID | 失效模式 | 影响 | 应对措施 |
390
- |--------|---------|------|---------|
391
- | RISK-01 | | | |
391
+ | 风险ID | 失效模式 | 影响 | 应对措施 | 验证场景 |
392
+ |--------|---------|------|---------|---------|
393
+ | RISK-01 | | | | E-01 |
392
394
 
393
395
  > 仅安全关键 / 高并发 / 强一致系统才升级为 FMEA:追加 S/O/D(1-10)与 RPN=S×O×D 三列,RPN≥200 上线前必须解决。普通功能用上表即可,不要为打分而打分。
394
396
 
@@ -457,19 +459,19 @@ flowchart TD
457
459
 
458
460
  ### 5.2 风险识别
459
461
 
460
- | 风险ID | 类型 | 描述 | 概率 | 影响 | 应对措施 |
461
- |--------|------|------|------|------|---------|
462
- | | | | | | |
462
+ | 风险ID | 类型 | 描述 | 概率 | 影响 | 应对措施 | 验证场景 |
463
+ |--------|------|------|------|------|---------|---------|
464
+ | | | | | | | S-/E-/B- 或手动验证理由 |
463
465
 
464
466
  ---
465
467
 
466
468
  ## 6. 需求追溯矩阵
467
469
 
468
- | 用户故事 | 功能ID | 接口ID | 测试用例ID | 状态 |
469
- |---------|--------|--------|-----------|------|
470
- | US-01 | FEAT-01 | API-01 | TC-001 | ✅ |
470
+ | 用户故事 | 功能ID | 接口ID | 测试用例ID | 测试层级 | 状态 |
471
+ |---------|--------|--------|-----------|---------|------|
472
+ | US-01 | FEAT-01 | API-01 | S-01 | E2E | 待实现 |
471
473
 
472
- > 用户故事一列引用 PRD 中的 US-XX(无 PRD 时填"需求描述");测试用例 ID 列引用 §2.5.2 的场景 ID(S-/E-/B-),不要另造编号。
474
+ > 用户故事一列引用 PRD 中的 US-XX(无 PRD 时填"需求描述");测试用例 ID 列引用 §2.5.2 的场景 ID(S-/E-/B-),不要另造编号。RULE 与高影响 RISK 必须映射到场景;无法自动化时写明手动验证原因,不得留空。
473
475
 
474
476
  ---
475
477
 
@@ -109,18 +109,20 @@
109
109
  #### 2.4.1 功能验收场景
110
110
 
111
111
  > 每个 P0/P1 FEAT 至少 1 条正常 + 1 条异常场景;"操作步骤/预期结果"写到可直接转为自动化测试断言的粒度(明确输入与可观测输出)。场景 ID(S-/E-/B-)即测试用例编号,供 plan 拆解引用。
112
+ >
113
+ > **测试层级**只能填 `unit` / `integration` / `E2E` / `manual`。跨 API、存储、运行时生成、渲染或用户可见结果等多个边界的场景必须标为 `E2E`;`manual` 仅限无法自动化的外部条件,并记录原因。**关键真实边界**列出测试中不得 mock 的组件或最终可观测面,编码阶段不得自行降级。
112
114
 
113
115
  **正常场景**
114
116
 
115
- | 场景ID | 功能ID | 优先级 | 操作步骤 | 预期结果 |
116
- |--------|--------|--------|---------|---------|
117
- | S-01 | FEAT-01 | P0 | 1. <步骤><br>2. <步骤> | <结果> |
117
+ | 场景ID | 功能ID | 优先级 | 测试层级 | 关键真实边界 | 操作步骤 | 预期结果 |
118
+ |--------|--------|--------|---------|-------------|---------|---------|
119
+ | S-01 | FEAT-01 | P0 | E2E | API → Store → 最终输出 | 1. <步骤><br>2. <步骤> | <结果> |
118
120
 
119
121
  **异常场景**
120
122
 
121
- | 场景ID | 功能ID | 触发条件 | 系统行为 |
122
- |--------|--------|---------|---------|
123
- | E-01 | FEAT-01 | | |
123
+ | 场景ID | 功能ID | 测试层级 | 关键真实边界 | 触发条件 | 系统行为 |
124
+ |--------|--------|---------|-------------|---------|---------|
125
+ | E-01 | FEAT-01 | integration | Service → Store | | |
124
126
 
125
127
  #### 2.4.2 非功能指标 [按需]
126
128
 
@@ -274,9 +276,9 @@ flowchart TD
274
276
 
275
277
  ### 4.2 风险识别
276
278
 
277
- | 风险ID | 描述 | 影响 | 应对措施 |
278
- |--------|------|------|---------|
279
- | | | | |
279
+ | 风险ID | 描述 | 影响 | 应对措施 | 验证场景 |
280
+ |--------|------|------|---------|---------|
281
+ | | | | | S-/E-/B- 或手动验证理由 |
280
282
 
281
283
  ---
282
284