@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,197 +0,0 @@
1
- """One executable plan gate for manifest identity, semantics and design closure."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import re
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
- from shlex import quote
9
- from typing import Mapping, Optional
10
-
11
-
12
- @dataclass(frozen=True)
13
- class FileSnapshot:
14
- """One file read once, with a stat fingerprint for drift detection."""
15
-
16
- path: str
17
- text: str
18
- size: int
19
- mtime_ns: int
20
-
21
- def drift(self) -> bool:
22
- try:
23
- stat = Path(self.path).stat()
24
- except OSError:
25
- return True
26
- return stat.st_size != self.size or stat.st_mtime_ns != self.mtime_ns
27
-
28
-
29
- @dataclass(frozen=True)
30
- class PlanSnapshot:
31
- """Verified inputs of one verify_plan operation; no persistent cache."""
32
-
33
- task: FileSnapshot
34
- manifest: FileSnapshot
35
- scenario_file: str
36
- scenario: Optional[FileSnapshot]
37
-
38
- def drift(self) -> bool:
39
- if self.task.drift() or self.manifest.drift():
40
- return True
41
- return self.scenario is not None and self.scenario.drift()
42
-
43
-
44
- def _read_snapshot(path: Path) -> FileSnapshot:
45
- stat = path.stat()
46
- return FileSnapshot(str(path), path.read_text(encoding="utf-8"), stat.st_size, stat.st_mtime_ns)
47
-
48
-
49
- def load_plan_snapshot(directory: str, task_file: str,
50
- require_scenarios: bool = False) -> PlanSnapshot:
51
- """Read TASK, manifest and scenario source at most once for one operation."""
52
- from codeflow.acceptance.cf_scenario_source import ScenarioError
53
- base = Path(directory)
54
- manifest = _read_snapshot(base / ".acceptance-manifest.json")
55
- task = _read_snapshot(Path(task_file))
56
- try:
57
- data = json.loads(manifest.text)
58
- except ValueError:
59
- data = {}
60
- try:
61
- source_file = _source(base, data if isinstance(data, Mapping) else {}, require_scenarios)
62
- except ScenarioError:
63
- source_file = ""
64
- source = _read_snapshot(Path(source_file)) if source_file and Path(source_file).is_file() else None
65
- return PlanSnapshot(task, manifest, source_file, source)
66
-
67
-
68
- def _root(directory: Path) -> Path:
69
- return next((parent.parent for parent in (directory, *directory.parents)
70
- if parent.name == ".code-flow"), directory)
71
-
72
-
73
- def _source(directory: Path, manifest: Mapping[str, object], required: bool) -> str:
74
- from codeflow.acceptance.cf_scenario_source import require_source
75
- named = manifest.get("scenarios_file")
76
- if isinstance(named, str) and named:
77
- return str(directory / named)
78
- candidates = sorted(directory.glob("*.scenarios.md"))
79
- if required or candidates:
80
- return str(require_source(directory))
81
- return ""
82
-
83
-
84
- def _projection(source: Mapping[str, object], manifest: Mapping[str, object]) -> list[dict[str, object]]:
85
- from codeflow.acceptance.cf_acceptance_schema import scenario_check_ids
86
- rows = {str(row["id"]): row for row in manifest.get("scenarios", [])}
87
- checks = {str(check["id"]) for check in manifest.get("checks", [])}
88
- findings: list[dict[str, object]] = []
89
- for row in source["scenarios"]:
90
- required = scenario_check_ids(row)
91
- current = rows.get(str(row["id"]), {})
92
- unknown = set(required) - checks
93
- if unknown:
94
- findings.append({"code": "unknown_check", "scenario": row["id"], "checks": sorted(unknown)})
95
- if required and set(required) != set(scenario_check_ids(current)):
96
- findings.append({"code": "scenario_checks_not_projected", "scenario": row["id"]})
97
- return findings
98
-
99
-
100
- def _scenario_snapshot(snapshot: PlanSnapshot, source_file: str) -> Optional[FileSnapshot]:
101
- scenario = snapshot.scenario
102
- if not source_file:
103
- return None
104
- if scenario is not None and Path(scenario.path).resolve() == Path(source_file).resolve():
105
- return scenario
106
- return _read_snapshot(Path(source_file)) if Path(source_file).is_file() else None
107
-
108
-
109
- def _scenario_ids(text: str) -> set[str]:
110
- """Expand explicit IDs and `S-01~04` / `S-01~S-04` ranges."""
111
- ids = {item.upper() for item in re.findall(r"\b(?:S|E|B)-\d+\b", text, re.IGNORECASE)}
112
- for match in re.finditer(r"\b([SEB])-(\d+)\s*~\s*(?:\1-)?(\d+)\b", text, re.IGNORECASE):
113
- prefix, start, end = match.group(1).upper(), int(match.group(2)), int(match.group(3))
114
- width = max(len(match.group(2)), len(match.group(3)))
115
- for value in range(start, end + 1):
116
- ids.add(f"{prefix}-{value:0{width}d}")
117
- return ids
118
-
119
-
120
- def _checklist_issues(text: str) -> list[dict[str, str]]:
121
- """Flag scenario IDs used in a Checklist but not declared in Acceptance-Refs.
122
-
123
- Cross-task boundaries start as this mismatch: a task writes another task's
124
- scenario into its checklist, then the plan assigns the scenario to only one
125
- owner. Reported as a non-blocking warning for existing plans.
126
- """
127
- from codeflow.task.cf_task_doc import TaskDocumentError, parse_task_document
128
- try:
129
- document = parse_task_document(text)
130
- except TaskDocumentError:
131
- return []
132
- issues: list[dict[str, str]] = []
133
- for task in document.tasks:
134
- declared = _scenario_ids(str(task.fields.get("Acceptance-Refs", "")))
135
- checklist = task.body.partition("### Checklist")[2].partition("### Acceptance Contract")[0]
136
- used = _scenario_ids(checklist)
137
- for scenario in sorted(used - declared):
138
- issues.append({"code": "checklist_scenario_not_in_refs", "task": task.id, "scenario": scenario})
139
- for scenario in sorted(declared - used):
140
- issues.append({"code": "acceptance_ref_not_in_checklist", "task": task.id, "scenario": scenario})
141
- return issues
142
-
143
-
144
- def verify_plan(directory: str, task_file: str, require_scenarios: bool = False,
145
- *, snapshot: Optional[PlanSnapshot] = None) -> dict[str, object]:
146
- from codeflow.acceptance.cf_acceptance_manifest_core import validate_manifest
147
- from codeflow.acceptance.cf_acceptance_schema import load_manifest
148
- from codeflow.acceptance.cf_check_registration import validate_registration
149
- from codeflow.cli.cf_design_lint import lint_requirement
150
- from codeflow.acceptance.cf_scenario_source import parse_scenarios, validate_e2e_quality
151
- from codeflow.spec.cf_spec_context_model import load_context
152
- from codeflow.spec.cf_stage_gate import result_to_data, validate_plan_coverage, validate_stage
153
- base = Path(directory)
154
- path = base / ".acceptance-manifest.json"
155
- if not path.is_file():
156
- return {"decision": "block", "reason": "acceptance_manifest_missing"}
157
- plan = snapshot if snapshot is not None else load_plan_snapshot(directory, task_file, require_scenarios)
158
- if plan.drift():
159
- return {"decision": "block", "reason": "plan_snapshot_drift"}
160
- try:
161
- manifest = load_manifest(path, text=plan.manifest.text)
162
- except (OSError, ValueError, json.JSONDecodeError) as exc:
163
- return {"decision": "block", "reason": f"acceptance_manifest_invalid: {exc}"}
164
- source_file = _source(base, manifest, require_scenarios)
165
- scenario = _scenario_snapshot(plan, source_file)
166
- valid, reason = validate_manifest(task_file, str(path), task_text=plan.task.text,
167
- manifest=manifest, scenario_file=source_file,
168
- scenario_text=scenario.text if scenario is not None else None)
169
- if not valid:
170
- return {"decision": "block", "reason": reason}
171
- if scenario is not None:
172
- source = parse_scenarios(scenario.path, text=scenario.text)
173
- elif source_file:
174
- source = parse_scenarios(source_file)
175
- else:
176
- source = {"scenarios": manifest["scenarios"]}
177
- quality = validate_e2e_quality(source["scenarios"])
178
- projection = _projection(source, manifest)
179
- registration = validate_registration(str(_root(base)), manifest["scenarios"], manifest.get("checks", []))
180
- quality.update({"runnable_smoke_count": registration["runnable_smoke_count"],
181
- "runnable_browser_count": registration["runnable_browser_count"],
182
- "registration_status": "passed" if registration["ok"] else "failed"})
183
- design = lint_requirement(task_file, source_file, task_text=plan.task.text,
184
- scenarios=source["scenarios"] if source_file else None)
185
- context = load_context(str(base / "spec-context.yml"))
186
- gate = validate_stage(context, "plan", root=str(_root(base)), task_dir=directory)
187
- coverage = validate_plan_coverage(context, task_file, text=plan.task.text)
188
- checklist = _checklist_issues(plan.task.text)
189
- blocked = (not quality["ok"] or projection or not registration["ok"] or
190
- design["decision"] != "pass" or gate.decision != "pass" or coverage.decision != "pass")
191
- recovery = (["code-flow spec refresh --task-dir " + quote(str(base)) +
192
- " --root " + quote(str(_root(base))) + " --json"]
193
- if any(issue.code == "rule_artifact_stale" for issue in gate.errors) else [])
194
- return {"decision": "block" if blocked else "pass", "gate": result_to_data(gate), "next": recovery,
195
- "coverage": result_to_data(coverage), "e2e_quality": quality,
196
- "registration": registration, "design_lint": design, "projection": projection,
197
- "checklist": checklist}
@@ -1,174 +0,0 @@
1
- """Shared semantic checks for canonical scenarios and plan projections."""
2
- from __future__ import annotations
3
-
4
- import re
5
- from typing import Mapping, Sequence
6
-
7
- ENVIRONMENTS = ("none", "local", "local-api", "browser", "external", "staging", "capacity", "ci", "production")
8
- TARGET_PHASES = ("plan", "code", "review", "ci", "release")
9
- EXTRA_FIELDS = ("expected_refs", "expected_checks", "assertions", "defer_reason", "owner", "target_phase", "features", "tags")
10
- _PLACEHOLDERS = frozenset(("", "-", "n/a", "na", "none", "无", "todo", "tbd", "待定"))
11
- _VAGUE = re.compile(r"可观|不回归|表现良好|符合预期|适当提升|as expected|significant improvement", re.I)
12
-
13
-
14
- def meaningful(value: object) -> bool:
15
- return str(value or "").strip().lower() not in _PLACEHOLDERS
16
-
17
-
18
- def issue(code: str, row: Mapping[str, object], field: str, message: str) -> dict[str, object]:
19
- return {"code": code, "scenario": str(row.get("id", "")), "field": field, "message": message}
20
-
21
-
22
- def validate_expected_checks(row: Mapping[str, object]) -> list[dict[str, object]]:
23
- """Validate explicit composition without weakening the scenario's checks."""
24
- links = row.get("expected_checks")
25
- expected, checks = row.get("expected", []), row.get("checks", [])
26
- if not isinstance(links, list) or not links or not isinstance(expected, list) or not isinstance(checks, list):
27
- raise ValueError("expected_checks 需要非空映射、expected 与 checks 列表")
28
- seen: set[int] = set()
29
- for link in links:
30
- if not isinstance(link, Mapping):
31
- raise ValueError("expected_checks 成员必须是映射")
32
- index, members = link.get("expected"), link.get("checks")
33
- if type(index) is not int or not 1 <= index <= len(expected) or index in seen:
34
- raise ValueError("expected_checks 的 expected 索引无效或重复")
35
- if (not isinstance(members, list) or not members
36
- or not all(isinstance(member, str) and member in checks for member in members)
37
- or len(set(members)) != len(members)):
38
- raise ValueError("expected_checks 引用了无效 check")
39
- seen.add(index)
40
- if seen != set(range(1, len(expected) + 1)):
41
- raise ValueError("expected_checks 必须覆盖所有 expected")
42
- return [dict(link) for link in links]
43
-
44
-
45
- def validate_extensions(row: Mapping[str, object]) -> dict[str, object]:
46
- env = str(row.get("env", "none")).lower()
47
- if env not in ENVIRONMENTS:
48
- raise ValueError(f"env 非法: {env!r}; allowed: {', '.join(ENVIRONMENTS)}")
49
- result: dict[str, object] = {"env": env}
50
- if "expected_checks" in row:
51
- result["expected_checks"] = validate_expected_checks(row)
52
- for field in ("features", "tags", "assertions"):
53
- values = row.get(field, [])
54
- if not isinstance(values, list) or not all(isinstance(value, str) and value.strip() for value in values):
55
- raise ValueError(f"{field} 必须是非空字符串列表")
56
- result[field] = list(values)
57
- for component in row.get("components", []) or []:
58
- if not isinstance(component, str) or not re.fullmatch(r"[a-zA-Z][a-zA-Z0-9_.:-]*", component):
59
- raise ValueError("components 必须使用稳定标识符,不能使用自由描述文本")
60
- refs = row.get("expected_refs", [])
61
- if not isinstance(refs, list) or not all(isinstance(value, Mapping) for value in refs):
62
- raise ValueError("expected_refs 必须是映射列表")
63
- result["expected_refs"] = [dict(value) for value in refs]
64
- for field in ("defer_reason", "owner", "target_phase"):
65
- value = row.get(field, "")
66
- if not isinstance(value, str):
67
- raise ValueError(f"{field} 必须是字符串")
68
- result[field] = value.strip()
69
- if result["target_phase"] and result["target_phase"] not in TARGET_PHASES:
70
- raise ValueError("target_phase 非法")
71
- return result
72
-
73
-
74
- def browser_boundary(row: Mapping[str, object]) -> bool:
75
- components = {str(value).lower() for value in row.get("components", []) or []}
76
- return str(row.get("env", "")).lower() == "browser" or bool(components & {"frontend", "browser"})
77
-
78
-
79
- def _expected_links(row: Mapping[str, object], index: int) -> bool:
80
- steps = row.get("steps", []) or []
81
- for link in row.get("expected_refs", []) or []:
82
- if not isinstance(link, Mapping) or type(link.get("expected")) is not int or link["expected"] != index:
83
- continue
84
- linked_steps = link.get("steps", [])
85
- assertions = link.get("assertions", [])
86
- if not isinstance(linked_steps, list) or not isinstance(assertions, list):
87
- continue
88
- valid_steps = linked_steps and all(type(value) is int and 1 <= value <= len(steps) for value in linked_steps)
89
- declared = row.get("assertions", [])
90
- valid_assertions = assertions and all(isinstance(value, str) and meaningful(value) and value in declared
91
- for value in assertions)
92
- if (valid_steps or valid_assertions) and (not linked_steps or valid_steps):
93
- return True
94
- return False
95
-
96
-
97
- def expectation_issues(row: Mapping[str, object]) -> list[dict[str, object]]:
98
- errors: list[dict[str, object]] = []
99
- for index, expected in enumerate(row.get("expected", []) or [], 1):
100
- text = str(expected).strip()
101
- if not meaningful(text) or _VAGUE.search(text):
102
- errors.append(issue("scenario_expected_nondeterministic", row, "expected",
103
- f"expected[{index}] 需要具体可观察结果或量化阈值"))
104
- if re.search(r"^(若|如果|if\b)", text, re.I) and not row.get("preconditions"):
105
- errors.append(issue("scenario_expected_nondeterministic", row, "preconditions",
106
- "条件式预期必须声明可构造的前置条件"))
107
- if not _expected_links(row, index):
108
- errors.append(issue("scenario_expected_reference_missing", row, "expected_refs",
109
- f"expected[{index}] 必须引用存在的步骤或稳定断言 ID"))
110
- return errors
111
-
112
-
113
- def deferred_issues(row: Mapping[str, object]) -> list[dict[str, object]]:
114
- if row.get("exec") != "deferred":
115
- return []
116
- missing = [field for field in ("defer_reason", "owner", "target_phase") if not meaningful(row.get(field))]
117
- return [issue("scenario_defer_metadata_missing", row, "exec", "延后信息缺少: " + ", ".join(missing))] if missing else []
118
-
119
-
120
- def e2e_issues(row: Mapping[str, object]) -> list[dict[str, object]]:
121
- errors: list[dict[str, object]] = []
122
- requirements = (("why_e2e", "e2e_justification_missing"), ("boundary", "e2e_boundary_missing"))
123
- for field, code in requirements:
124
- if not meaningful(row.get(field)):
125
- errors.append(issue(code, row, field, "E2E 必须声明真实边界和低层测试不足的理由"))
126
- for field in ("steps", "expected"):
127
- if not any(meaningful(value) for value in row.get(field, []) or []):
128
- errors.append(issue(f"e2e_{field}_missing", row, field, "E2E 必须声明可复现步骤及可观察结果"))
129
- if str(row.get("priority", "")).upper() not in ("P0", "P1"):
130
- errors.append(issue("e2e_priority_not_critical", row, "priority", "E2E 只允许 P0/P1"))
131
- if not browser_boundary(row):
132
- errors.append(issue("e2e_browser_boundary_missing", row, "components",
133
- "浏览器 E2E 必须声明 browser/frontend;API/CLI 全链路使用 system/integration"))
134
- if browser_boundary(row) and not re.search(r"browser|frontend|浏览器|前端|\bUI\b", str(row.get("boundary", "")), re.I):
135
- errors.append(issue("scenario_boundary_components_mismatch", row, "boundary",
136
- "声明的浏览器组件必须出现在真实边界中"))
137
- if row.get("exec") == "aspirational":
138
- errors.append(issue("e2e_aspirational", row, "exec", "aspirational 不能作为 E2E 验收"))
139
- paths = row.get("trigger_paths", []) or []
140
- if not row.get("smoke") and not paths:
141
- errors.append(issue("e2e_trigger_paths_missing", row, "trigger_paths", "非 smoke E2E 必须声明触发路径"))
142
- if any(not meaningful(value) for value in paths):
143
- errors.append(issue("e2e_trigger_path_placeholder", row, "trigger_paths", "触发路径不能使用占位值"))
144
- return errors
145
-
146
-
147
- def quality_report(scenarios: Sequence[Mapping[str, object]]) -> dict[str, object]:
148
- errors: list[dict[str, object]] = []
149
- warnings: list[dict[str, object]] = []
150
- e2e = [row for row in scenarios if str(row.get("level", "")).lower() == "e2e"]
151
- signatures: dict[str, str] = {}
152
- for row in scenarios:
153
- errors.extend(expectation_issues(row))
154
- errors.extend(deferred_issues(row))
155
- if row in e2e:
156
- errors.extend(e2e_issues(row))
157
- signature = repr((row.get("steps"), row.get("expected"), row.get("boundary")))
158
- if signature in signatures:
159
- warnings.append(issue("duplicate_e2e_journey", row, "steps", f"与 {signatures[signature]} 重复"))
160
- signatures[signature] = str(row.get("id"))
161
- elif meaningful(row.get("why_e2e")):
162
- errors.append(issue("why_e2e_on_non_e2e", row, "why_e2e", "why_e2e 只属于浏览器 E2E"))
163
- ratio = len(e2e) / len(scenarios) if scenarios else 0.0
164
- smoke = sum(bool(row.get("smoke")) for row in e2e)
165
- if ratio > 0.3:
166
- warnings.append(issue("e2e_ratio_high", {}, "level", f"E2E 占比 {ratio:.0%};优先下沉低层"))
167
- if e2e and not smoke:
168
- warnings.append(issue("e2e_smoke_missing", {}, "smoke", "没有声明 smoke E2E"))
169
- return {"ok": not errors, "errors": errors, "warnings": warnings, "total": len(scenarios),
170
- "e2e_count": len(e2e), "e2e_browser_count": sum(browser_boundary(row) for row in e2e),
171
- "system_count": sum(row.get("level") == "system" for row in scenarios),
172
- "capacity_count": sum(row.get("level") == "capacity" or "capacity" in row.get("tags", []) for row in scenarios),
173
- "smoke_count": smoke, "e2e_ratio": round(ratio, 4),
174
- "runnable_smoke_count": None, "verified_browser_count": None, "registration_status": "not_checked"}