@michaelxwb/code-flow 0.8.1 → 1.0.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 (268) hide show
  1. package/README.md +73 -152
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +156 -1417
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +18 -21
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +12 -12
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/acceptance-contract.md +0 -217
  54. package/docs/ci/acceptance-evidence.md +0 -77
  55. package/docs/migrations/runtime-0.7.0.md +0 -41
  56. package/docs/migrations/runtime-0.8.0.md +0 -154
  57. package/docs/migrations/runtime-0.9.0.md +0 -40
  58. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  59. package/docs/spec-injection-hardening.md +0 -54
  60. package/docs/spec-injection-performance.json +0 -73
  61. package/docs/spec-optimization.md +0 -12
  62. package/src/adapters/claude/CLAUDE.md +0 -45
  63. package/src/adapters/claude/commands/cf-init.md +0 -262
  64. package/src/adapters/claude/commands/cf-learn.md +0 -291
  65. package/src/adapters/claude/commands/cf-spec.md +0 -87
  66. package/src/adapters/claude/commands/cf-stats.md +0 -96
  67. package/src/adapters/claude/commands/cf-sync.md +0 -39
  68. package/src/adapters/claude/commands/cf-task/align.md +0 -331
  69. package/src/adapters/claude/commands/cf-task/archive.md +0 -155
  70. package/src/adapters/claude/commands/cf-task/block.md +0 -49
  71. package/src/adapters/claude/commands/cf-task/graph.md +0 -92
  72. package/src/adapters/claude/commands/cf-task/note.md +0 -248
  73. package/src/adapters/claude/commands/cf-task/plan.md +0 -477
  74. package/src/adapters/claude/commands/cf-task/prd.md +0 -173
  75. package/src/adapters/claude/commands/cf-task/start.md +0 -317
  76. package/src/adapters/claude/commands/cf-task/status.md +0 -71
  77. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
  78. package/src/adapters/claude/commands/cf-validate.md +0 -100
  79. package/src/adapters/claude/settings.local.json +0 -50
  80. package/src/adapters/codex/AGENTS.md +0 -45
  81. package/src/adapters/codex/config.toml +0 -3
  82. package/src/adapters/codex/hooks.json +0 -54
  83. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  84. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  85. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
  86. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
  87. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
  88. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
  89. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
  90. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
  91. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
  92. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
  93. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
  94. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
  95. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
  96. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
  97. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
  98. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
  99. package/src/adapters/commands.json +0 -115
  100. package/src/adapters/costrict/CLAUDE.md +0 -45
  101. package/src/adapters/costrict/commands/cf-init.md +0 -262
  102. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  103. package/src/adapters/costrict/commands/cf-spec.md +0 -87
  104. package/src/adapters/costrict/commands/cf-stats.md +0 -96
  105. package/src/adapters/costrict/commands/cf-sync.md +0 -39
  106. package/src/adapters/costrict/commands/cf-task/align.md +0 -331
  107. package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
  108. package/src/adapters/costrict/commands/cf-task/block.md +0 -49
  109. package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
  110. package/src/adapters/costrict/commands/cf-task/note.md +0 -248
  111. package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
  112. package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
  113. package/src/adapters/costrict/commands/cf-task/start.md +0 -317
  114. package/src/adapters/costrict/commands/cf-task/status.md +0 -71
  115. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
  116. package/src/adapters/costrict/commands/cf-validate.md +0 -100
  117. package/src/adapters/costrict/settings.local.json +0 -50
  118. package/src/adapters/install-plan.json +0 -100
  119. package/src/adapters/opencode/AGENTS.md +0 -45
  120. package/src/adapters/opencode/commands/cf-init.md +0 -281
  121. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  122. package/src/adapters/opencode/commands/cf-spec.md +0 -91
  123. package/src/adapters/opencode/commands/cf-stats.md +0 -100
  124. package/src/adapters/opencode/commands/cf-sync.md +0 -44
  125. package/src/adapters/opencode/commands/cf-task/align.md +0 -331
  126. package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
  127. package/src/adapters/opencode/commands/cf-task/block.md +0 -54
  128. package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
  129. package/src/adapters/opencode/commands/cf-task/note.md +0 -253
  130. package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
  131. package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
  132. package/src/adapters/opencode/commands/cf-task/start.md +0 -317
  133. package/src/adapters/opencode/commands/cf-task/status.md +0 -76
  134. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
  135. package/src/adapters/opencode/commands/cf-validate.md +0 -105
  136. package/src/adapters/opencode/opencode.json +0 -3
  137. package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
  138. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  139. package/src/adapters/platform-bindings.json +0 -370
  140. package/src/cli.js +0 -1076
  141. package/src/core/code-flow/config.yml +0 -161
  142. package/src/core/code-flow/runtime-commands.json +0 -259
  143. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
  144. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
  145. package/src/core/code-flow/scripts/cf_audit.py +0 -10
  146. package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
  147. package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
  148. package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
  149. package/src/core/code-flow/scripts/cf_feedback.py +0 -10
  150. package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
  151. package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
  152. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
  153. package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
  154. package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
  155. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
  156. package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
  157. package/src/core/code-flow/scripts/cf_stats.py +0 -10
  158. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
  159. package/src/core/code-flow/scripts/cf_sync.py +0 -10
  160. package/src/core/code-flow/scripts/cf_task_index.py +0 -10
  161. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
  162. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
  163. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
  164. package/src/core/code-flow/scripts/cf_validation.py +0 -10
  165. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  166. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  167. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +0 -347
  168. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
  169. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
  170. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
  171. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
  172. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
  173. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
  174. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
  175. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
  176. package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
  177. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
  178. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
  179. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
  180. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
  181. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  182. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
  183. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
  184. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
  185. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
  186. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
  187. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
  188. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
  189. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
  190. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
  191. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
  192. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
  193. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
  194. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
  195. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
  196. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
  197. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
  198. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  199. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
  200. package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
  201. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
  202. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
  203. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
  204. package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
  205. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
  206. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
  207. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
  208. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
  209. package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
  210. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
  211. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
  212. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
  213. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
  214. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  215. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
  216. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  217. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
  218. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
  219. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
  220. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
  221. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
  222. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
  223. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  224. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
  225. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
  226. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
  227. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
  228. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
  229. package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
  230. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
  231. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
  232. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
  233. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
  234. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
  235. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
  236. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
  237. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
  238. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
  239. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
  240. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
  241. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
  242. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
  243. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
  244. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  245. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
  246. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
  247. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
  248. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
  249. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
  250. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
  251. package/src/core/code-flow/scripts/runtime-resources.json +0 -29
  252. package/src/core/code-flow/specs/backend/_map.md +0 -56
  253. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  254. package/src/core/code-flow/specs/backend/database.md +0 -51
  255. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  256. package/src/core/code-flow/specs/backend/logging.md +0 -50
  257. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  258. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  259. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  260. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  261. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  262. package/src/core/code-flow/specs/shared/_map.md +0 -57
  263. package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
  264. package/src/core/code-flow/tasks/.gitkeep +0 -0
  265. package/src/core/code-flow/validation.yml +0 -59
  266. package/src/migrate/runtime.js +0 -213
  267. package/src/migrate/spec-workflow.js +0 -253
  268. package/src/runtime.js +0 -219
@@ -1,300 +0,0 @@
1
- """Parse and reconcile canonical requirement scenario sources."""
2
- from __future__ import annotations
3
-
4
- import re
5
- from pathlib import Path
6
- from typing import Mapping, Optional, Sequence
7
-
8
- import yaml
9
-
10
- from codeflow.core.cf_markdown import frontmatter_block
11
-
12
-
13
- SCHEMA_VERSION = 1
14
- SCENARIO_ID = re.compile(r"^[SEB]-\d+$")
15
- PRIORITIES = ("P0", "P1", "P2")
16
- LEVELS = ("unit", "component", "contract", "integration", "system", "capacity", "e2e", "manual")
17
- EXEC_MODES = ("auto", "deferred", "manual", "aspirational")
18
- E2E_CRITICAL_PRIORITIES = ("P0", "P1")
19
- E2E_RATIO_WARNING = 0.3
20
- _PLACEHOLDERS = frozenset(("", "-", "n/a", "na", "none", "无", "todo", "tbd", "待定"))
21
-
22
-
23
- class ScenarioError(ValueError):
24
- """Stable parse/merge failure with a machine-readable code."""
25
-
26
- def __init__(self, code: str, message: str, details: Optional[Mapping[str, object]] = None) -> None:
27
- super().__init__(message)
28
- self.code = code
29
- self.message = message
30
- self.details = dict(details) if details else {}
31
-
32
-
33
- def _frontmatter_text(text: str) -> str:
34
- """YAML body for `yaml.safe_load`; unclosed delimiters fail closed."""
35
- block = frontmatter_block(text)
36
- if block is not None:
37
- return block
38
- if text.startswith("---"):
39
- raise ScenarioError("scenario_source_invalid_frontmatter", "frontmatter 缺少闭合 ---")
40
- return text
41
-
42
-
43
- def _mapping(value: object, field: str) -> Mapping[str, object]:
44
- if not isinstance(value, Mapping):
45
- raise ScenarioError("invalid_scenario_source", f"{field} 必须是 mapping")
46
- return value
47
-
48
-
49
- def _string_list(value: object, field: str) -> tuple[str, ...]:
50
- if value is None:
51
- return ()
52
- if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
53
- raise ScenarioError("invalid_scenario_source", f"{field} 必须是字符串列表")
54
- return tuple(item.strip() for item in value if item.strip())
55
-
56
-
57
- def _boolean(value: object, field: str, default: bool = False) -> bool:
58
- if value is None:
59
- return default
60
- if not isinstance(value, bool):
61
- raise ScenarioError("invalid_scenario_source", f"{field} 必须是布尔值")
62
- return value
63
-
64
-
65
- def _meaningful(value: object) -> bool:
66
- return str(value or "").strip().lower() not in _PLACEHOLDERS
67
-
68
-
69
- def _validate_one(item: Mapping[str, object]) -> dict[str, object]:
70
- scenario_id = item.get("id")
71
- if not isinstance(scenario_id, str) or not SCENARIO_ID.fullmatch(scenario_id):
72
- raise ScenarioError("invalid_scenario_id", f"非法场景 ID: {scenario_id!r}")
73
- priority = str(item.get("priority", "P1")).upper()
74
- if priority not in PRIORITIES:
75
- raise ScenarioError("invalid_priority", f"{scenario_id}: priority 非法: {priority!r}")
76
- level = str(item.get("level", "integration")).lower()
77
- if level not in LEVELS:
78
- raise ScenarioError("invalid_level", f"{scenario_id}: level 非法: {level!r}")
79
- exec_mode = str(item.get("exec", "auto")).lower()
80
- if exec_mode not in EXEC_MODES:
81
- raise ScenarioError("invalid_exec", f"{scenario_id}: exec 非法: {exec_mode!r}")
82
- try:
83
- from codeflow.acceptance.cf_scenario_quality import validate_extensions
84
- extra = validate_extensions(item)
85
- except ValueError as exc:
86
- raise ScenarioError("invalid_scenario_contract", f"{scenario_id}: {exc}") from exc
87
- return {
88
- **extra,
89
- "id": scenario_id,
90
- "title": str(item.get("title", "")).strip(),
91
- "priority": priority,
92
- "level": level,
93
- "exec": exec_mode,
94
- "env": extra["env"],
95
- "components": list(_string_list(item.get("components"), f"{scenario_id}.components")),
96
- "boundary": str(item.get("boundary", "")).strip(),
97
- "preconditions": list(_string_list(item.get("preconditions"), f"{scenario_id}.preconditions")),
98
- "steps": list(_string_list(item.get("steps"), f"{scenario_id}.steps")),
99
- "expected": list(_string_list(item.get("expected"), f"{scenario_id}.expected")),
100
- "design_refs": list(_string_list(item.get("design_refs"), f"{scenario_id}.design_refs")),
101
- "rules": list(_string_list(item.get("rules"), f"{scenario_id}.rules")),
102
- "checks": list(_string_list(item.get("checks"), f"{scenario_id}.checks")),
103
- "sources": list(_string_list(item.get("sources"), f"{scenario_id}.sources")),
104
- "why_e2e": str(item.get("why_e2e", "")).strip(),
105
- "trigger_paths": list(_string_list(item.get("trigger_paths"), f"{scenario_id}.trigger_paths")),
106
- "smoke": _boolean(item.get("smoke"), f"{scenario_id}.smoke"),
107
- }
108
-
109
-
110
- def parse_scenarios(path: object, *, text: Optional[str] = None) -> dict[str, object]:
111
- """Parse and validate one scenarios source file."""
112
- target = Path(path)
113
- try:
114
- raw = target.read_text(encoding="utf-8") if text is None else text
115
- except (OSError, UnicodeError) as exc:
116
- raise ScenarioError("scenario_source_unreadable", str(exc), {"path": str(target)}) from exc
117
- try:
118
- data = yaml.safe_load(_frontmatter_text(raw))
119
- except yaml.YAMLError as exc:
120
- raise ScenarioError("scenario_source_invalid_yaml", str(exc), {"path": str(target)}) from exc
121
- document = _mapping(data, "scenarios")
122
- schema = document.get("schema")
123
- if isinstance(schema, bool) or schema not in (None, SCHEMA_VERSION):
124
- raise ScenarioError("unsupported_scenario_schema", str(document.get("schema")))
125
- raw = document.get("scenarios")
126
- if not isinstance(raw, list) or not raw:
127
- raise ScenarioError("scenarios_missing", "scenarios 必须是非空列表")
128
- seen: set[str] = set()
129
- scenarios: list[dict[str, object]] = []
130
- for item in raw:
131
- scenario = _validate_one(_mapping(item, "scenarios[]"))
132
- if scenario["id"] in seen:
133
- raise ScenarioError("duplicate_scenario_id", str(scenario["id"]))
134
- seen.add(scenario["id"])
135
- scenarios.append(scenario)
136
- return {
137
- "schema": SCHEMA_VERSION,
138
- "requirement": str(document.get("requirement", target.parent.name)),
139
- "source": str(target),
140
- "scenarios": scenarios,
141
- }
142
-
143
-
144
- def _definition(scenario: Mapping[str, object]) -> dict[str, object]:
145
- fields = ("title", "priority", "level", "exec", "env", "components", "boundary",
146
- "preconditions", "steps", "expected", "rules", "why_e2e", "trigger_paths", "smoke")
147
- from codeflow.acceptance.cf_scenario_quality import EXTRA_FIELDS
148
- return {field: scenario.get(field) for field in (*fields, *EXTRA_FIELDS)}
149
-
150
-
151
- def merge_scenarios(sets: Sequence[Mapping[str, object]]) -> list[dict[str, object]]:
152
- """Union scenario sets by global ID; reject conflicting definitions."""
153
- merged: dict[str, dict[str, object]] = {}
154
- for source in sets:
155
- for raw in source.get("scenarios", []) or []:
156
- scenario = _validate_one(_mapping(raw, "scenarios[]"))
157
- current = merged.get(str(scenario["id"]))
158
- if current is None:
159
- merged[str(scenario["id"])] = scenario
160
- continue
161
- if _definition(current) != _definition(scenario):
162
- fields = [key for key in _definition(current) if _definition(current)[key] != _definition(scenario)[key]]
163
- raise ScenarioError(
164
- "design_scenario_conflict",
165
- f"{scenario['id']} 定义冲突: {', '.join(fields)}",
166
- {"scenario": scenario["id"], "fields": fields},
167
- )
168
- current["sources"] = sorted(set(current.get("sources", [])) | set(scenario.get("sources", [])))
169
- current["design_refs"] = sorted(set(current.get("design_refs", [])) | set(scenario.get("design_refs", [])))
170
- current["checks"] = sorted(set(current.get("checks", [])) | set(scenario.get("checks", [])))
171
- return [merged[key] for key in sorted(merged)]
172
-
173
-
174
- def _quality_issue(code: str, scenario: str, field: str, message: str) -> dict[str, object]:
175
- return {"code": code, "scenario": scenario, "field": field, "message": message}
176
-
177
-
178
- def validate_e2e_quality(scenarios: Sequence[Mapping[str, object]]) -> dict[str, object]:
179
- """Validate semantic acceptance contracts through one shared quality gate."""
180
- from codeflow.acceptance.cf_scenario_quality import quality_report
181
- return quality_report(scenarios)
182
-
183
-
184
- def scenario_status(scenario: Mapping[str, object], checks: Mapping[str, str]) -> str:
185
- """Derive scenario state from all required checks (AND semantics)."""
186
- required = [str(item) for item in scenario.get("checks", [])]
187
- if not required:
188
- return "aspirational" if scenario.get("exec") == "aspirational" else "designed"
189
- states = [checks.get(check_id, "designed") for check_id in required]
190
- for bad in ("failed", "env_blocked", "incomplete"):
191
- if bad in states:
192
- return bad
193
- if all(state in ("passed", "verified") for state in states):
194
- return "verified"
195
- return "designed"
196
-
197
-
198
- def require_source(task_dir: object) -> Path:
199
- """Return the single scenarios source for a requirement directory."""
200
- directory = Path(task_dir)
201
- matches = sorted(directory.glob("*.scenarios.md")) if directory.is_dir() else []
202
- if not matches:
203
- raise ScenarioError("scenario_source_missing", f"缺少 <需求>.scenarios.md: {directory}")
204
- if len(matches) > 1:
205
- raise ScenarioError("scenario_source_ambiguous", f"多个场景源: {', '.join(path.name for path in matches)}")
206
- return matches[0]
207
-
208
-
209
- def reconcile_coverage(scenarios: Sequence[Mapping[str, object]],
210
- coverage: Sequence[Mapping[str, object]]) -> dict[str, object]:
211
- """Reconcile canonical scenarios with Acceptance Coverage rows."""
212
- by_scenario = {str(item.get("id")): item for item in scenarios}
213
- owners: dict[str, list[str]] = {}
214
- unknown: list[str] = []
215
- for row in coverage:
216
- scenario_id = row.get("scenario_id") or row.get("id")
217
- if not isinstance(scenario_id, str) or not scenario_id:
218
- continue
219
- if scenario_id not in by_scenario:
220
- unknown.append(scenario_id)
221
- continue
222
- owner = row.get("owner")
223
- if isinstance(owner, str) and owner.strip():
224
- owners.setdefault(scenario_id, []).append(owner.strip())
225
- covered = set(owners) | {str(row.get("scenario_id") or row.get("id")) for row in coverage
226
- if isinstance(row.get("scenario_id") or row.get("id"), str)}
227
- missing = sorted(set(by_scenario) - covered)
228
- duplicate = sorted(key for key, values in owners.items() if len(set(values)) > 1)
229
- unowned = sorted(set(by_scenario) - set(owners))
230
- return {
231
- "ok": not missing and not unknown and not duplicate and not unowned,
232
- "missing": missing,
233
- "unknown": sorted(unknown),
234
- "duplicate_owner": duplicate,
235
- "unowned": unowned,
236
- }
237
-
238
-
239
- def load_requirement_scenarios(task_dir: object, allow_legacy: bool = False) -> dict[str, object]:
240
- """Load the requirement source, or an explicit deprecated legacy marker."""
241
- directory = Path(task_dir)
242
- try:
243
- source = require_source(directory)
244
- except ScenarioError as exc:
245
- if exc.code != "scenario_source_missing" or not allow_legacy:
246
- raise
247
- return {"schema": SCHEMA_VERSION, "requirement": directory.name,
248
- "source": "", "scenarios": [], "deprecated": True}
249
- return parse_scenarios(source)
250
-
251
-
252
- def audit_scenario_source(scenarios: Sequence[Mapping[str, object]],
253
- coverage: Sequence[Mapping[str, object]],
254
- manifest_ids: Optional[Sequence[str]] = None) -> dict[str, object]:
255
- """Three-way audit: scenarios, coverage, manifest."""
256
- report = reconcile_coverage(scenarios, coverage)
257
- scenario_ids = {str(item.get("id")) for item in scenarios}
258
- coverage_ids = {str(item.get("scenario_id") or item.get("id")) for item in coverage}
259
- manifest = {str(item) for item in manifest_ids} if manifest_ids is not None else None
260
- report["manifest_missing"] = sorted(scenario_ids - manifest) if manifest is not None else []
261
- report["manifest_unknown"] = sorted(manifest - scenario_ids) if manifest is not None else []
262
- report["coverage_unknown"] = sorted(coverage_ids - scenario_ids)
263
- report["ok"] = bool(report["ok"] and not report["coverage_unknown"]
264
- and not report["manifest_missing"] and not report["manifest_unknown"])
265
- return report
266
-
267
-
268
- def migrate_from_coverage(coverage: Sequence[Mapping[str, object]]) -> dict[str, object]:
269
- """Build an initial scenarios source from legacy Acceptance Coverage."""
270
- scenarios: list[dict[str, object]] = []
271
- invalid: list[str] = []
272
- for row in coverage:
273
- scenario_id = str(row.get("scenario_id") or row.get("id") or "")
274
- if not SCENARIO_ID.fullmatch(scenario_id):
275
- invalid.append(scenario_id)
276
- continue
277
- priority = str(row.get("priority") or "P1").upper()
278
- level = str(row.get("level") or "integration").lower()
279
- if priority not in PRIORITIES:
280
- priority = "P1"
281
- if level not in LEVELS:
282
- level = "integration"
283
- scenarios.append({
284
- "id": scenario_id,
285
- "title": str(row.get("title") or scenario_id),
286
- "priority": priority,
287
- "level": level,
288
- "exec": "auto",
289
- "env": "none",
290
- "components": [],
291
- "boundary": str(row.get("boundary") or ""),
292
- "preconditions": [],
293
- "steps": [],
294
- "expected": [],
295
- "design_refs": [],
296
- "rules": [],
297
- "checks": [],
298
- "sources": ["legacy-coverage"],
299
- })
300
- return {"schema": SCHEMA_VERSION, "scenarios": scenarios, "migrated": True, "invalid": invalid}