@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,867 +0,0 @@
1
- """Shared requirement verification for CLI and parallel batch orchestration."""
2
- from __future__ import annotations
3
-
4
- from pathlib import Path
5
- import re
6
- import sys
7
- import time
8
- from typing import Mapping, Optional, Sequence
9
-
10
- from codeflow.acceptance.cf_acceptance_manifest_core import validate_execution_baseline
11
- from codeflow.acceptance.cf_acceptance_risk import accepted_risk
12
- from codeflow.acceptance.cf_acceptance_schema import load_manifest, scenario_check_ids, verified_evidence
13
- from codeflow.acceptance.cf_acceptance_selection import select_owned, select_profile
14
- from codeflow.cli.cf_acceptance_runner import run_manifest
15
- from codeflow.cli.cf_task_index import project_task_nodes
16
- from codeflow.core import cf_git
17
- from codeflow.core.cf_exec_base import record_report, report_session, worktree_fingerprint
18
- from codeflow.spec.cf_active_task import load_active_task
19
- from codeflow.spec.cf_spec_context_model import ContextError, SpecContext, context_sha256, load_context, save_context
20
- from codeflow.spec.cf_spec_metadata import SpecMetadata, load_spec_metadata
21
- from codeflow.spec.cf_spec_verify import (VerificationEvidence, VerificationResult, VerificationScope, run_all_verifiers,
22
- store_verification_evidence)
23
- from codeflow.spec.cf_stage_gate import validate_stage
24
- from codeflow.spec.cf_workflow_service import FINISHED_STATUSES, complete_requirement_verification
25
- from codeflow.spec.cf_workflow_transaction import recover_transition
26
- from codeflow.task.cf_task_doc import TaskDocumentError, parse_task_document
27
- from codeflow.task.cf_task_runtime import integrate_verification_evidence
28
-
29
-
30
- def demand_tasks(directory: Path) -> list[tuple[Path, str, str]]:
31
- tasks = []
32
- seen = set()
33
- for task_file in sorted(directory.glob("*.md")):
34
- if task_file.name.endswith((".design.md", ".prd.md", ".scenarios.md")):
35
- continue
36
- try:
37
- text = task_file.read_text(encoding="utf-8")
38
- except (OSError, UnicodeError) as exc:
39
- raise ValueError(f"task_read_error: {exc}") from exc
40
- try:
41
- document = parse_task_document(text, source=str(task_file))
42
- except TaskDocumentError as exc:
43
- if exc.code == "task_document_empty":
44
- continue
45
- raise ValueError(str(exc)) from exc
46
- for node in project_task_nodes(document):
47
- if node.task_id in seen:
48
- raise ValueError(f"duplicate task id: {node.task_id}")
49
- seen.add(node.task_id)
50
- tasks.append((task_file, node.task_id, node.status))
51
- if not tasks:
52
- raise ValueError("no TASK sections found")
53
- return tasks
54
-
55
-
56
- def _head_token(root: str) -> str:
57
- try:
58
- return "head:" + cf_git.run_git(root, ("rev-parse", "HEAD")).strip()
59
- except cf_git.GitError:
60
- return "head:none"
61
-
62
-
63
- def _review_scope(root: str) -> VerificationScope:
64
- files = set(cf_git.tracked_files(root))
65
- try:
66
- files.update(cf_git.changed_files(root))
67
- except cf_git.GitError:
68
- pass
69
- return VerificationScope(root, tuple(sorted(files)), _head_token(root))
70
-
71
-
72
- def collect_requirement_bindings(directory: Path,
73
- stages: Sequence[str] = ("review",)) -> dict[str, dict[str, object]]:
74
- """收集需求目录下全部 task context 中指定阶段的绑定。
75
-
76
- 返回 spec_id -> {"path": spec 相对路径, "rules": {rule_ref: [(context_path, context), ...]}}。
77
- 读取失败 fail-closed(报告文件与原因),不允许静默跳过。
78
- """
79
- allowed = set(stages)
80
- targets: dict[str, dict[str, object]] = {}
81
- for ctx_path in sorted(directory.rglob("spec-context.yml")):
82
- try:
83
- context = load_context(str(ctx_path))
84
- except (ContextError, OSError, ValueError) as exc:
85
- raise ValueError(f"review_context_unreadable: {ctx_path}: {exc}") from exc
86
- for binding in context.bindings:
87
- for rule in binding.rules:
88
- if rule.verifier_stage not in allowed:
89
- continue
90
- entry = targets.setdefault(binding.spec_id, {"path": binding.path, "rules": {}})
91
- entry["rules"].setdefault(rule.ref, []).append((ctx_path, context))
92
- return targets
93
-
94
-
95
- def collect_review_bindings(directory: Path) -> dict[str, dict[str, object]]:
96
- """review 层绑定(manual 确认与待确认清单专用)。"""
97
- return collect_requirement_bindings(directory, ("review",))
98
-
99
-
100
- def _confirmed_manual(spec_id: str, entries: Sequence[tuple[Path, SpecContext]],
101
- rule_ref: str) -> Optional[Mapping[str, object]]:
102
- """从任意绑定该 rule 的 context 读取有效的用户确认记录(按 rule 自身 stage)。"""
103
- for _, context in entries:
104
- for binding in context.bindings:
105
- if binding.spec_id != spec_id:
106
- continue
107
- for rule in binding.rules:
108
- if rule.ref != rule_ref:
109
- continue
110
- status = rule.stage_status.get(rule.verifier_stage or "code")
111
- decision = status.decision if status is not None else None
112
- if (status is not None and status.status == "verified"
113
- and decision is not None and decision.kind == "manual_verification"):
114
- return {
115
- "reason": decision.reason,
116
- "confirmed_by": decision.confirmed_by,
117
- "confirmed_at": decision.confirmed_at,
118
- "source": decision.source,
119
- }
120
- return None
121
-
122
-
123
- def pending_manual_rules(
124
- root: str, targets: Mapping[str, Mapping[str, object]]
125
- ) -> tuple[list[dict[str, object]], dict[str, dict[str, Mapping[str, object]]]]:
126
- """收集 review 层 manual 规则:待用户确认清单 + 已确认项的 verifier 入参。"""
127
- pending: list[dict[str, object]] = []
128
- confirmations: dict[str, dict[str, Mapping[str, object]]] = {}
129
- for spec_id in sorted(targets):
130
- info = targets[spec_id]
131
- metadata = load_spec_metadata(str(Path(root) / ".code-flow/specs" / str(info["path"])))
132
- manual = {item.rule: item for item in metadata.verifiers if item.type == "manual"}
133
- for rule_ref, entries in sorted(info["rules"].items()):
134
- confirmation = _confirmed_manual(spec_id, entries, rule_ref)
135
- if confirmation is not None:
136
- confirmations.setdefault(spec_id, {})[rule_ref] = confirmation
137
- elif rule_ref in manual:
138
- verifier = manual[rule_ref]
139
- pending.append({
140
- "ref": f"{spec_id}#{rule_ref}",
141
- "spec_id": spec_id,
142
- "checklist": verifier.config.get("checklist", ""),
143
- "owner": verifier.config.get("owner", ""),
144
- "bound_contexts": len(entries),
145
- })
146
- return pending, confirmations
147
-
148
-
149
- def _manual_confirmations(root: str, targets: Mapping[str, Mapping[str, object]]) -> dict[str, dict[str, Mapping[str, object]]]:
150
- """收集各 manual rule 自身 stage 上已记录的用户确认(verifier 入参)。"""
151
- confirmations: dict[str, dict[str, Mapping[str, object]]] = {}
152
- for spec_id in sorted(targets):
153
- info = targets[spec_id]
154
- metadata = load_spec_metadata(str(Path(root) / ".code-flow/specs" / str(info["path"])))
155
- manual_refs = {item.rule for item in metadata.verifiers if item.type == "manual"}
156
- for rule_ref, entries in sorted(info["rules"].items()):
157
- if rule_ref not in manual_refs:
158
- continue
159
- confirmation = _confirmed_manual(spec_id, entries, rule_ref)
160
- if confirmation is not None:
161
- confirmations.setdefault(spec_id, {})[rule_ref] = confirmation
162
- return confirmations
163
-
164
-
165
- def _store_requirement_evidence(metadata: SpecMetadata, scope: VerificationScope,
166
- result: VerificationResult, directory: str) -> None:
167
- try:
168
- from codeflow.evidence.cf_evidence_store import requirement_key
169
- store_verification_evidence(metadata, scope, result, requirement=requirement_key(directory))
170
- except (OSError, ValueError) as exc:
171
- sys.stderr.write(f"cf_task_workflow: verifier evidence sidecar failed: {exc}\n")
172
-
173
-
174
- def _update_requirement_contexts(evidence_by_context: Mapping[Path, list[VerificationEvidence]],
175
- scope: VerificationScope) -> list[str]:
176
- # Integrate all specs before writing each context to avoid stale overwrites.
177
- for ctx_path in sorted(evidence_by_context, key=str):
178
- context = load_context(str(ctx_path))
179
- updated, _ = integrate_verification_evidence(context, evidence_by_context[ctx_path])
180
- if updated != context:
181
- save_context(str(ctx_path), updated)
182
- failures = []
183
- for ctx_path in sorted(evidence_by_context):
184
- gate = validate_stage(load_context(str(ctx_path)), "review", diff_sha256=scope.diff_sha256)
185
- failures.extend(f"{issue.spec_id}#{issue.rule_ref}" for issue in gate.errors)
186
- return failures
187
-
188
-
189
- def _summarize_verifier(evidence: VerificationEvidence, summary: list[dict[str, object]]) -> str:
190
- summary.append({"check_id": evidence.verifier_ref, "status": evidence.status, "error_code": evidence.error_code})
191
- if evidence.status == "verified":
192
- return "reused" if (evidence.details or {}).get("cache_reused") else "executed"
193
- if evidence.error_code in _ENV_ERROR_CODES:
194
- return "env_blocked"
195
- return "incomplete" if evidence.error_code == "verifier_timeout" else "failed"
196
-
197
-
198
- def _run_requirement_verifiers(
199
- root: str,
200
- directory: str,
201
- targets: dict[str, dict[str, object]],
202
- confirmations: Optional[Mapping[str, Mapping[str, Mapping[str, object]]]] = None,
203
- ) -> dict[str, object]:
204
- """执行需求级全量 verifier(code+review 按 spec/rule 去重),证据写回全部相关 context。"""
205
- executed = reused = env_blocked = incomplete = 0
206
- failures: list[str] = []
207
- summaries: list[dict[str, object]] = []
208
- evidence_by_context: dict[Path, list[VerificationEvidence]] = {}
209
- started = time.monotonic()
210
- scope = _review_scope(root)
211
- preflight_ms = (time.monotonic() - started) * 1000
212
- planning_ms = evidence_ms = 0.0
213
- for spec_id in sorted(targets):
214
- info = targets[spec_id]
215
- started = time.monotonic()
216
- metadata = load_spec_metadata(str(Path(root) / ".code-flow/specs" / str(info["path"])))
217
- planning_ms += (time.monotonic() - started) * 1000
218
- result = run_all_verifiers(metadata, scope, (confirmations or {}).get(spec_id), stage=None)
219
- started = time.monotonic()
220
- _store_requirement_evidence(metadata, scope, result, directory)
221
- evidence_ms += (time.monotonic() - started) * 1000
222
- by_rule: dict[str, VerificationEvidence] = {}
223
- for evidence in result.evidence:
224
- rule_ref = evidence.verifier_ref.split("#", 1)[-1]
225
- by_rule[rule_ref] = evidence
226
- category = _summarize_verifier(evidence, summaries)
227
- executed += category == "executed"
228
- reused += category == "reused"
229
- env_blocked += category == "env_blocked"
230
- incomplete += category == "incomplete"
231
- if category == "failed":
232
- failures.append(evidence.verifier_ref)
233
- for rule_ref, entries in info["rules"].items():
234
- evidence = by_rule.get(rule_ref)
235
- if evidence is None:
236
- continue
237
- for ctx_path, _context in entries:
238
- evidence_by_context.setdefault(ctx_path, []).append(evidence)
239
- started = time.monotonic()
240
- failures.extend(_update_requirement_contexts(evidence_by_context, scope))
241
- evidence_ms += (time.monotonic() - started) * 1000
242
- record_report({"kind": "spec_phases", "timings": {"planning_ms": planning_ms,
243
- "preflight_ms": preflight_ms, "evidence_ms": evidence_ms}})
244
- return {"executed": executed, "reused": reused, "failed": sorted(set(failures)),
245
- "env_blocked": env_blocked, "incomplete": incomplete, "evidence": summaries}
246
-
247
-
248
- VERIFY_LAYERS = ("validators", "review", "e2e")
249
- VERIFY_PROFILES = ("smoke", "changed", "full")
250
- _ENV_ERROR_CODES = frozenset(("env_blocked", "environment_unavailable", "command_unavailable"))
251
-
252
-
253
- def parse_verify_layers(value: Optional[str]) -> tuple[str, ...]:
254
- if value is None or not value.strip():
255
- return VERIFY_LAYERS
256
- chosen: set[str] = set()
257
- for item in value.split(","):
258
- name = item.strip()
259
- if not name:
260
- continue
261
- if name not in VERIFY_LAYERS:
262
- raise ValueError(f"unknown verify layer: {name}; allowed: {', '.join(VERIFY_LAYERS)}")
263
- chosen.add(name)
264
- if not chosen:
265
- raise ValueError(f"--layers requires at least one of: {', '.join(VERIFY_LAYERS)}")
266
- return tuple(name for name in VERIFY_LAYERS if name in chosen)
267
-
268
-
269
- def parse_verify_profile(value: Optional[str], default: str = "changed") -> str:
270
- profile = (value or default).strip()
271
- if profile not in VERIFY_PROFILES:
272
- raise ValueError(f"unknown verify profile: {profile}; allowed: {', '.join(VERIFY_PROFILES)}")
273
- return profile
274
-
275
-
276
- def _layer_result(decision: str, *, reason: str = "", executed: int = 0, reused: int = 0,
277
- failed: Optional[Sequence[str]] = None, env_blocked: int = 0, incomplete: int = 0,
278
- evidence: Optional[Sequence[Mapping[str, object]]] = None,
279
- **extra: object) -> dict[str, object]:
280
- result: dict[str, object] = {
281
- "decision": decision,
282
- "reason": reason,
283
- "executed": executed,
284
- "reused": reused,
285
- "failed": list(failed or ()),
286
- "env_blocked": env_blocked,
287
- "incomplete": incomplete,
288
- "evidence": list(evidence or ()),
289
- }
290
- result.update(extra)
291
- return result
292
-
293
-
294
- def _nothing_to_verify() -> dict[str, object]:
295
- return _layer_result("pass", reason="nothing_to_verify")
296
-
297
-
298
- def _validation_view(details: Mapping[str, object]) -> dict[str, object]:
299
- return {
300
- "decision": details.get("decision"),
301
- "reason": details.get("reason", ""),
302
- "reused": details.get("reused", 0),
303
- "failures": details.get("failures", []),
304
- "files": list(details.get("files") or ()),
305
- "timings": details.get("timings"),
306
- "observations": details.get("observations", []),
307
- }
308
-
309
-
310
- def _validator_execution_count(root: str, selected: Sequence[str], reused: int) -> int:
311
- from codeflow.core.cf_validators import resolve_validators, trigger_matches
312
-
313
- _config, validators = resolve_validators(root)
314
- applicable = 0
315
- for validator in validators:
316
- if not isinstance(validator, Mapping):
317
- continue
318
- trigger = str(validator.get("trigger", ""))
319
- if any(trigger_matches(trigger, path) for path in selected):
320
- applicable += 1
321
- return max(0, applicable - reused)
322
-
323
-
324
- def _validators_layer(root: str, profile: str,
325
- files: Optional[Sequence[str]] = None) -> dict[str, object]:
326
- try:
327
- from codeflow.task.cf_validation_service import validate_files
328
-
329
- if files is None:
330
- from codeflow.task.cf_validation_service import validation_scope
331
- files = (tuple(sorted(cf_git.tracked_files(root))) if profile == "full"
332
- else validation_scope(root))
333
- details = validate_files(root, files, include_heavy=profile == "full")
334
- except (OSError, ValueError) as exc:
335
- return _layer_result("block", reason="validation_error", detail=str(exc))
336
- view = _validation_view(details)
337
- if details.get("reason") in ("no_changes", "no_validators_configured"):
338
- return _layer_result("pass", reason="nothing_to_verify", validation=view)
339
- failures = list(details.get("failures") or ())
340
- failed = [str(item.get("name", "validator")) for item in failures if not item.get("incomplete")]
341
- incomplete = sum(1 for item in failures if item.get("incomplete"))
342
- reused = int(details.get("reused") or 0)
343
- selected = details.get("files") or ()
344
- executed = _validator_execution_count(root, selected, reused)
345
- decision = "pass" if details.get("decision") == "pass" else "block"
346
- return _layer_result(
347
- decision,
348
- reason="" if decision == "pass" else "validation_failed",
349
- executed=executed,
350
- reused=reused,
351
- failed=failed,
352
- incomplete=incomplete,
353
- validation=view,
354
- )
355
-
356
-
357
- def _validation_report(result: dict[str, object], layer: Mapping[str, object],
358
- layers: Sequence[str]) -> dict[str, object]:
359
- view = layer.get("validation") or {}
360
- observations = view.get("observations") or []
361
- result["scope"] = {"layers": list(layers), "files": view.get("files", [])}
362
- result["selection_reason"] = ("validator_trigger_matches_selected_files" if observations
363
- else "no_validator_selected")
364
- result["reuse_reason"] = "matching_validation_cache" if layer.get("reused") else "no_reused_results"
365
- result["execution_identities"] = [item["identity"] for item in observations]
366
- result["input_change_category"] = sorted({item["input_change_category"] for item in observations})
367
- result["summary"] = (f"validators: executed {int(layer.get('executed') or 0)}, "
368
- f"reused {int(layer.get('reused') or 0)}")
369
- if view.get("timings") is not None:
370
- result["timings"] = dict(view["timings"])
371
- result["timings_scope"] = ["validators"]
372
- return result
373
-
374
-
375
- def _combine_acceptance_timing(result: dict[str, object], layer: Mapping[str, object],
376
- started: float) -> None:
377
- acceptance = layer.get("acceptance")
378
- if not isinstance(acceptance, Mapping) or not isinstance(acceptance.get("timings"), Mapping):
379
- return
380
- incoming = acceptance["timings"]
381
- current = result.get("timings")
382
- if not isinstance(current, dict):
383
- current = {key: 0.0 for key in ("planning_ms", "preflight_ms", "execution_ms", "evidence_ms")}
384
- result["timings"] = current
385
- result["timings_scope"] = []
386
- for key in ("planning_ms", "preflight_ms", "execution_ms", "evidence_ms"):
387
- current[key] = float(current.get(key, 0)) + float(incoming.get(key, 0))
388
- current["elapsed_ms"] = (time.monotonic() - started) * 1000
389
- result["timings_scope"].append("e2e")
390
- reasons = {"e2e": acceptance.get("selection_reason", "unknown")}
391
- if "selection_reason" in result:
392
- reasons["validators"] = result["selection_reason"]
393
- result["selection_reasons"] = reasons
394
- result["selection_reason"] = "multiple_layer_policies" if len(reasons) > 1 else reasons["e2e"]
395
- result["selected_check_ids"] = acceptance.get("selected_check_ids", [])
396
- result.setdefault("execution_identities", []).extend(acceptance.get("execution_identities", []))
397
- categories = acceptance.get("input_change_category") or {}
398
- result["input_change_categories"] = {"validators": result.get("input_change_category", []), "e2e": categories}
399
- result["input_change_category"] = sorted(set(result.get("input_change_category", [])) | set(categories.values()))
400
- summaries = [str(value) for value in (result.get("summary"), acceptance.get("summary")) if value]
401
- result["summary"] = "; ".join(summaries)
402
- result["reuse_reason"] = "evidence_reused" if result.get("reused") else "no_reused_results"
403
- if "scope" not in result:
404
- result["scope"] = {"layers": result.get("selected_layers", []), "acceptance": acceptance.get("scope", {})}
405
-
406
-
407
- def _verify_validators(root: str, directory: str, profile: str) -> dict[str, object]:
408
- """Development checks use the active baseline without completing the TASK."""
409
- started = time.monotonic()
410
- files: Optional[Sequence[str]] = None
411
- try:
412
- if (Path(root) / ".code-flow/.active-task.json").exists():
413
- active = load_active_task(root)
414
- if (Path(root) / active.task_dir).resolve() != Path(directory).resolve():
415
- raise ValueError("active_task_directory_mismatch")
416
- context = load_context(str(Path(directory) / "spec-context.yml"))
417
- if active.context_sha256 != context_sha256(context):
418
- raise ValueError("active_context_hash_mismatch: run code-flow spec refresh/doctor")
419
- from codeflow.task.cf_validation_service import validation_scope
420
- files = validation_scope(root)
421
- layer = _validators_layer(root, profile, files)
422
- except (OSError, ValueError) as exc:
423
- layer = _layer_result("block", reason="validation_context_invalid", detail=str(exc))
424
- result = _aggregate_layers({"validators": layer}, profile, ("validators",))
425
- _validation_report(result, layer, ("validators",))
426
- if "timings" in result:
427
- result["timings"]["elapsed_ms"] = (time.monotonic() - started) * 1000
428
- return result
429
-
430
-
431
- def _review_layer(root: str, directory: str) -> dict[str, object]:
432
- started = time.monotonic()
433
- try:
434
- review_targets = collect_review_bindings(Path(directory))
435
- pending_rules, _ = pending_manual_rules(root, review_targets)
436
- except ValueError as exc:
437
- return _layer_result("block", reason="manual_review_context_unreadable", detail=str(exc))
438
- if pending_rules:
439
- return _layer_result(
440
- "block",
441
- reason="manual_confirmation_required",
442
- manual_rules=pending_rules,
443
- next=("向用户展示待确认清单并取得明确回复后,运行 confirm-manual 批量写入确认,再重跑 verify;"
444
- "Agent 不得代确认。"),
445
- )
446
- try:
447
- all_targets = collect_requirement_bindings(Path(directory), ("code", "review"))
448
- except ValueError as exc:
449
- return _layer_result("block", reason="review_context_unreadable", detail=str(exc))
450
- if not all_targets:
451
- return _nothing_to_verify()
452
- confirmations = _manual_confirmations(root, all_targets)
453
- record_report({"kind": "spec_phases", "timings": {"planning_ms": (time.monotonic() - started) * 1000}})
454
- review = _run_requirement_verifiers(root, directory, all_targets, confirmations)
455
- blocked = bool(review["failed"] or review["env_blocked"] or review["incomplete"])
456
- return _layer_result(
457
- "block" if blocked else "pass",
458
- reason="review_failed" if blocked else "",
459
- executed=int(review["executed"]),
460
- reused=int(review["reused"]),
461
- failed=review["failed"],
462
- env_blocked=int(review["env_blocked"]),
463
- incomplete=int(review["incomplete"]),
464
- evidence=review["evidence"],
465
- review={"executed": review["executed"], "reused": review["reused"], "failed": review["failed"]},
466
- )
467
-
468
-
469
- def _acceptance_counts(acceptance: Mapping[str, object]) -> dict[str, object]:
470
- executed = reused = incomplete = env_blocked = 0
471
- failed: list[str] = []
472
- evidence: list[dict[str, object]] = []
473
- for item in acceptance.get("results") or ():
474
- if not isinstance(item, Mapping):
475
- continue
476
- scenario_id = str(item.get("id", ""))
477
- status = str(item.get("status", ""))
478
- evidence.append({"check_id": scenario_id, "status": status})
479
- if status == "incomplete":
480
- incomplete += 1
481
- elif status == "env_blocked":
482
- env_blocked += 1
483
- elif status in ("failed", "not_configured"):
484
- failed.append(scenario_id)
485
- elif status == "passed":
486
- if item.get("cache_reused"):
487
- reused += 1
488
- else:
489
- executed += 1
490
- return {"executed": executed, "reused": reused, "failed": sorted(failed),
491
- "env_blocked": env_blocked, "incomplete": incomplete, "evidence": evidence}
492
-
493
-
494
- def e2e_preflight(manifest: Path, tasks: Sequence[tuple[Path, str, str]],
495
- requirement_gate: bool, profile: str = "changed",
496
- root: str = "") -> tuple[Optional[dict[str, object]], Optional[dict[str, object]]]:
497
- try:
498
- data = load_manifest(manifest)
499
- validate_execution_baseline(manifest, data)
500
- except (OSError, ValueError) as exc:
501
- return None, _layer_result("block", reason="acceptance_manifest_invalid", detail=str(exc))
502
- bound_task = (manifest.parent / str(data.get("task_file", ""))).resolve()
503
- if {task.resolve() for task, _, _ in tasks} != {bound_task}:
504
- return None, _layer_result("block", reason="acceptance_manifest_task_mismatch")
505
- owner = ",".join(task for _, task, _ in tasks)
506
- selected_ids: Optional[set[str]] = None
507
- if requirement_gate and root and profile != "full":
508
- selected = select_profile(data, root, profile)["scenarios"]
509
- selected_ids = {str(item.get("id")) for item in select_owned(selected, owner)}
510
- data = {**data, "scenarios": select_owned(data["scenarios"], owner)}
511
- if not requirement_gate:
512
- if not any(row.get("kind") == "e2e" or row.get("exec") == "deferred" for row in data["scenarios"]):
513
- return None, _nothing_to_verify()
514
- return data, None
515
- functional_required, manual_scenarios = _pending_scenario_rows(data, selected_ids)
516
- if functional_required:
517
- return None, _layer_result("block", reason="functional_or_manual_evidence_missing",
518
- scenarios=functional_required)
519
- if manual_scenarios:
520
- return None, _layer_result(
521
- "block", reason="manual_confirmation_required", manual_scenarios=manual_scenarios,
522
- next=("向用户展示待确认清单并取得明确回复后,运行 confirm-manual 批量写入确认,再重跑 verify;"
523
- "Agent 不得代确认。"))
524
- return data, None
525
-
526
-
527
- def _pending_scenario_rows(data: Mapping[str, object], selected_ids: Optional[set[str]]
528
- ) -> tuple[list[object], list[dict[str, object]]]:
529
- functional_required: list[object] = []
530
- manual_scenarios: list[dict[str, object]] = []
531
- checks = {
532
- str(item.get("id")): item for item in data.get("checks", [])
533
- if isinstance(item, Mapping) and item.get("id")
534
- }
535
- for row in data["scenarios"]:
536
- if row.get("exec") == "aspirational":
537
- continue
538
- if selected_ids is not None and str(row.get("id")) not in selected_ids:
539
- continue
540
- if row.get("status") == "stale":
541
- # Stale evidence is known-outdated, not missing. The E2E layer
542
- # re-runs it; preflight must not turn it into a hard archive block.
543
- continue
544
- if verified_evidence(row):
545
- continue
546
- if row.get("status") == "accepted_risk":
547
- check_ids = scenario_check_ids(row)
548
- if check_ids and any(accepted_risk(checks.get(check_id), row) is not None for check_id in check_ids):
549
- continue
550
- if row.get("kind") == "manual" or row.get("exec") == "manual":
551
- manual_scenarios.append({
552
- "id": row.get("id"),
553
- "owner": row.get("owner", ""),
554
- "boundary": row.get("boundary", ""),
555
- })
556
- elif (row.get("status") == "verified" and row.get("kind", "functional") != "e2e"
557
- and row.get("exec") != "deferred"):
558
- functional_required.append(row["id"])
559
- return functional_required, manual_scenarios
560
-
561
-
562
- def stale_functional_rows(data: Mapping[str, object], root: str) -> list[str]:
563
- """Functional rows claiming verified but recorded against different content."""
564
- from codeflow.acceptance.cf_acceptance_evidence import stale_scenario_checks
565
- fingerprint = worktree_fingerprint(root)
566
- checks = {
567
- str(item.get("id")): item for item in data.get("checks", [])
568
- if isinstance(item, Mapping) and item.get("id")
569
- }
570
- from codeflow.acceptance.cf_check_inputs import check_input_hashes
571
- per_check = check_input_hashes(root, checks, fingerprint)
572
- stale: list[str] = []
573
- for row in data.get("scenarios", []) or []:
574
- if not isinstance(row, Mapping):
575
- continue
576
- if str(row.get("kind", "functional")) in ("manual", "e2e"):
577
- continue
578
- if row.get("status") not in ("verified", "stale"):
579
- continue
580
- if stale_scenario_checks(row, per_check, fingerprint):
581
- stale.append(str(row.get("id")))
582
- return stale
583
-
584
-
585
- def _unverified_functional_rows(data: Mapping[str, object]) -> list[str]:
586
- checks = {
587
- str(item.get("id")): item for item in data.get("checks", [])
588
- if isinstance(item, Mapping) and item.get("id")
589
- }
590
- rows: list[str] = []
591
- for row in data.get("scenarios", []) or []:
592
- if not isinstance(row, Mapping):
593
- continue
594
- if str(row.get("kind", "functional")) in ("manual", "e2e") or row.get("exec") in ("manual", "aspirational"):
595
- continue
596
- if row.get("status") == "accepted_risk":
597
- check_ids = scenario_check_ids(row)
598
- if any(accepted_risk(checks.get(check_id), row) is not None for check_id in check_ids):
599
- continue
600
- if not verified_evidence(row):
601
- rows.append(str(row.get("id")))
602
- return rows
603
-
604
-
605
- def _acceptance_required(tasks: Sequence[tuple[Path, str, str]]) -> bool:
606
- for task_file, task_id, _ in tasks:
607
- try:
608
- text = Path(task_file).read_text(encoding="utf-8")
609
- except (OSError, UnicodeError):
610
- continue
611
- section = re.search(rf"(?ms)^##\s+{re.escape(task_id)}:.*?(?=^##\s+TASK-|\Z)", text)
612
- if section is None:
613
- continue
614
- body = section.group(0)
615
- refs = re.search(r"(?m)^- \*\*Acceptance-Refs\*\*:[ \t]*([^\n]*)", body)
616
- if refs and refs.group(1).strip().upper() not in ("", "N/A"):
617
- return True
618
- return False
619
-
620
-
621
- def _e2e_layer(root: str, directory: str, tasks: Sequence[tuple[Path, str, str]],
622
- requirement_gate: bool, profile: str = "changed", resume: bool = True,
623
- deadline: Optional[float] = None, max_failures: int = 0,
624
- fail_fast: bool = False, jobs: int = 0,
625
- partial_ids: Optional[Sequence[str]] = None) -> dict[str, object]:
626
- manifest = Path(directory) / ".acceptance-manifest.json"
627
- if not manifest.exists():
628
- if _acceptance_required(tasks):
629
- return _layer_result("block", reason="acceptance_manifest_missing")
630
- return _nothing_to_verify()
631
- data, failure = e2e_preflight(manifest, tasks, requirement_gate, profile, root)
632
- if failure is not None:
633
- return failure
634
- stale = stale_functional_rows(data, root) if requirement_gate else []
635
- unverified = _unverified_functional_rows(data) if requirement_gate else []
636
- if profile == "smoke":
637
- # Smoke only needs the E2E subset; functional freshness is handled by
638
- # the explicit changed/full gate, not by the batch smoke.
639
- stale = []
640
- unverified = []
641
- try:
642
- acceptance = run_manifest(
643
- str(manifest), root, write_evidence=True,
644
- owner=",".join(str(item[1]) for item in tasks),
645
- include_e2e=bool(stale or unverified) or profile in ("full", "smoke"),
646
- only_e2e=(partial_ids is not None and profile == "smoke") or (
647
- not (stale or unverified) and profile not in ("full", "smoke")),
648
- profile=profile, resume=resume, deadline=deadline,
649
- max_failures=max_failures, fail_fast=fail_fast, jobs=jobs,
650
- force_ids=tuple(partial_ids) if partial_ids is not None else tuple(stale or unverified),
651
- only_scenarios=partial_ids,
652
- )
653
- except (OSError, ValueError) as exc:
654
- return _layer_result("block", reason="acceptance_run_error", detail=str(exc))
655
- counts = _acceptance_counts(acceptance)
656
- decision = "pass" if acceptance.get("decision") == "pass" else "block"
657
- extra: dict[str, object] = {"results": acceptance.get("results", [])}
658
- for key in ("checks", "error", "env_blocked_details"):
659
- if key in acceptance:
660
- extra[key] = acceptance[key]
661
- return _layer_result(
662
- decision,
663
- reason="" if decision == "pass" else "e2e_failed",
664
- executed=counts["executed"],
665
- reused=counts["reused"],
666
- failed=counts["failed"],
667
- env_blocked=counts["env_blocked"],
668
- incomplete=counts["incomplete"],
669
- evidence=counts["evidence"],
670
- acceptance=acceptance,
671
- **extra,
672
- )
673
-
674
-
675
- def _aggregate_layers(layer_results: Mapping[str, Mapping[str, object]], profile: str,
676
- selected: Sequence[str]) -> dict[str, object]:
677
- blocking = [(name, layer) for name, layer in layer_results.items() if layer.get("decision") != "pass"]
678
- every_layer_empty = bool(layer_results) and all(
679
- layer.get("reason") == "nothing_to_verify" for layer in layer_results.values()
680
- )
681
- if blocking:
682
- first = blocking[0]
683
- reason = str(first[1].get("reason") or f"{first[0]}_failed")
684
- elif every_layer_empty:
685
- reason = "nothing_to_verify"
686
- else:
687
- reason = ""
688
- evidence = [
689
- {"layer": name, **item}
690
- for name, layer in layer_results.items()
691
- for item in layer.get("evidence", [])
692
- if isinstance(item, Mapping)
693
- ]
694
- result: dict[str, object] = {
695
- "decision": "block" if blocking else "pass",
696
- "reason": reason,
697
- "profile": profile,
698
- "selected_layers": list(selected),
699
- "layers": dict(layer_results),
700
- "executed": sum(int(layer.get("executed", 0)) for layer in layer_results.values()),
701
- "reused": sum(int(layer.get("reused", 0)) for layer in layer_results.values()),
702
- "failed": sorted({str(item) for layer in layer_results.values()
703
- for item in layer.get("failed", [])}),
704
- "env_blocked": sum(int(layer.get("env_blocked", 0)) for layer in layer_results.values()),
705
- "incomplete": sum(int(layer.get("incomplete", 0)) for layer in layer_results.values()),
706
- "evidence": evidence,
707
- "env_blocked_details": [
708
- {"layer": name, **item} for name, layer in layer_results.items()
709
- for item in layer.get("env_blocked_details", []) if isinstance(item, Mapping)
710
- ],
711
- }
712
- for field in ("manual_rules", "manual_scenarios", "scenarios", "next"):
713
- merged = [layer[field] for layer in layer_results.values() if field in layer]
714
- if merged:
715
- result[field] = merged[0] if len(merged) == 1 else merged
716
- return result
717
-
718
-
719
- def _batch_scope(root: str, directory: str, tasks: Sequence[tuple[Path, str, str]], wanted: set[str]
720
- ) -> tuple[list[tuple[Path, str, str]], list[str], list[str]]:
721
- manifest = Path(directory) / ".acceptance-manifest.json"
722
- if not manifest.is_file():
723
- return [task for task in tasks if task[1] in wanted], [], []
724
- data = load_manifest(manifest)
725
- rows = {str(row["id"]): row for row in data["scenarios"]}
726
- from codeflow.acceptance.cf_acceptance_selection import changed_paths
727
- from codeflow.acceptance.cf_check_inputs import match_path
728
- changes = changed_paths(root)
729
- checks = {str(item["id"]): item for item in data.get("checks", [])}
730
- has_explicit_smoke = any(row.get("kind") == "e2e" and row.get("smoke") for row in rows.values())
731
- selected = {sid for sid, row in rows.items() if row.get("owner") in wanted
732
- and (row.get("kind") != "e2e" or row.get("smoke") or not has_explicit_smoke)}
733
- for sid, row in rows.items():
734
- patterns = list(row.get("trigger_paths") or [])
735
- for check_id in scenario_check_ids(row):
736
- check = checks.get(check_id, {})
737
- patterns.extend(check.get("inputs") or [])
738
- patterns.extend(check.get("paths") or [])
739
- affected = any(match_path(path, str(pattern)) for path in changes for pattern in patterns)
740
- if (row.get("kind") == "e2e" and row.get("smoke")
741
- and (set(row.get("depends_on", [])) & selected or affected)):
742
- selected.add(sid)
743
- pending = list(selected)
744
- while pending:
745
- for dependency in rows[pending.pop()].get("depends_on", []):
746
- if dependency in rows and dependency not in selected:
747
- selected.add(dependency)
748
- pending.append(dependency)
749
- owners = wanted | {str(rows[sid].get("owner")) for sid in selected if rows[sid].get("owner")}
750
- scoped = [task for task in tasks if task[1] in owners]
751
- ready = {task_id for _, task_id, status in scoped if status in FINISHED_STATUSES}
752
- return scoped, sorted(selected), sorted(owners - ready)
753
-
754
-
755
- def _verify_layers(root: str, directory: str, layers: Optional[str] = None,
756
- profile: Optional[str] = None,
757
- only_tasks: Optional[Sequence[str]] = None,
758
- resume: bool = True, deadline: Optional[float] = None,
759
- max_failures: int = 0, fail_fast: bool = False,
760
- jobs: int = 0) -> dict[str, object]:
761
- """Run chosen layers; only_tasks limits the batch smoke gate to merged TASKs."""
762
- started = time.monotonic()
763
- recover_transition(root)
764
- selected = parse_verify_layers(layers)
765
- resolved_profile = parse_verify_profile(profile)
766
- if selected == ("validators",) and only_tasks is None:
767
- return _verify_validators(root, directory, resolved_profile)
768
- tasks = demand_tasks(Path(directory))
769
- if only_tasks is not None:
770
- wanted = {str(task_id) for task_id in only_tasks}
771
- known = {task_id for _, task_id, _ in tasks}
772
- if wanted - known:
773
- return {"decision": "block", "reason": "unknown_tasks", "tasks": sorted(wanted - known)}
774
- tasks, partial_ids, not_ready = _batch_scope(root, directory, tasks, wanted)
775
- if not_ready:
776
- return {"decision": "block", "reason": "not_ready", "tasks": not_ready,
777
- "scope": {"type": "partial", "tasks": sorted(wanted)}, "requirement_verified": False}
778
- else:
779
- partial_ids = None
780
- unfinished = [task_id for _, task_id, status in tasks if status not in FINISHED_STATUSES]
781
- if unfinished:
782
- return {"decision": "block", "reason": "unfinished_tasks", "tasks": unfinished}
783
- if (Path(root) / ".code-flow/.active-task.json").exists():
784
- return {"decision": "block", "reason": "finish_active_task_first"}
785
- requirement_gate = only_tasks is None and set(selected) == set(VERIFY_LAYERS)
786
- layer_results: dict[str, dict[str, object]] = {}
787
- for name in selected:
788
- if name == "validators":
789
- layer_results[name] = _validators_layer(root, resolved_profile)
790
- elif name == "review":
791
- layer_results[name] = _review_layer(root, directory)
792
- else:
793
- layer_results[name] = _e2e_layer(
794
- root, directory, tasks, requirement_gate,
795
- profile=resolved_profile, resume=resume, deadline=deadline,
796
- max_failures=max_failures, fail_fast=fail_fast, jobs=jobs, partial_ids=partial_ids,
797
- )
798
- result = _aggregate_layers(layer_results, resolved_profile, selected)
799
- if "validators" in layer_results:
800
- _validation_report(result, layer_results["validators"], selected)
801
- if "e2e" in layer_results:
802
- _combine_acceptance_timing(result, layer_results["e2e"], started)
803
- if only_tasks is not None:
804
- result["scope"] = {**result.get("scope", {}), "type": "partial", "tasks": sorted(wanted),
805
- "scenarios": partial_ids}
806
- result["requirement_verified"] = False
807
- if (requirement_gate and resolved_profile == "full" and result["decision"] == "pass"
808
- and (result.get("reason") != "nothing_to_verify" or not _acceptance_required(tasks))):
809
- return complete_requirement_verification(root, directory, tasks, result)
810
- return result
811
-
812
-
813
- def _append_spec_reporting(result: dict[str, object], reports: Sequence[Mapping[str, object]],
814
- started: float) -> None:
815
- checks = sorted((dict(item) for item in reports if item.get("kind") == "spec_check"),
816
- key=lambda item: str(item.get("check_id", "")))
817
- if not checks:
818
- return
819
- result["spec_checks"] = checks
820
- spec_executed = sum(1 for item in checks if not item.get("reused"))
821
- spec_reused = len(checks) - spec_executed
822
- review = result.get("layers", {}).get("review")
823
- if isinstance(review, dict):
824
- result["executed"] += spec_executed - int(review.get("executed", 0))
825
- result["reused"] += spec_reused - int(review.get("reused", 0))
826
- review["executed"], review["reused"] = spec_executed, spec_reused
827
- if isinstance(review.get("review"), dict):
828
- review["review"].update(executed=spec_executed, reused=spec_reused)
829
- prefix = str(result.get("summary") or "")
830
- result["summary"] = (prefix + "; " if prefix else "") + f"spec: executed {spec_executed}, reused {spec_reused}"
831
- result.setdefault("execution_identities", []).extend(item["identity"] for item in checks if item.get("identity"))
832
- timings = result.setdefault("timings", {key: 0.0 for key in
833
- ("planning_ms", "preflight_ms", "execution_ms", "evidence_ms")})
834
- timings["execution_ms"] += sum(float(item.get("execution_ms", 0)) for item in checks)
835
- for report in reports:
836
- if report.get("kind") == "spec_phases":
837
- phases = report.get("timings") or {}
838
- for key in ("planning_ms", "preflight_ms", "evidence_ms"):
839
- timings[key] += float(phases.get(key, 0))
840
- timings["elapsed_ms"] = (time.monotonic() - started) * 1000
841
- result.setdefault("timings_scope", []).append("spec")
842
- result.setdefault("scope", {"layers": result.get("selected_layers", [])})
843
- result.setdefault("input_change_category", ["unchanged" if spec_reused else "unproven"])
844
- if "selection_reason" in result:
845
- reasons = result.setdefault("selection_reasons", {})
846
- if not reasons:
847
- reasons["validators" if "validators" in result.get("timings_scope", []) else "other"] = result["selection_reason"]
848
- reasons["spec"] = "required_spec_bindings"
849
- result["selection_reason"] = "multiple_layer_policies"
850
- else:
851
- result["selection_reason"] = "required_spec_bindings"
852
- if any(item.get("reused") for item in checks):
853
- result["reuse_reason"] = "evidence_reused"
854
- else:
855
- result.setdefault("reuse_reason", "no_reused_results")
856
-
857
-
858
- def verify_layers(root: str, directory: str, layers: Optional[str] = None,
859
- profile: Optional[str] = None, only_tasks: Optional[Sequence[str]] = None,
860
- resume: bool = True, deadline: Optional[float] = None, max_failures: int = 0,
861
- fail_fast: bool = False, jobs: int = 0) -> dict[str, object]:
862
- started = time.monotonic()
863
- with report_session() as reports:
864
- result = _verify_layers(root, directory, layers, profile, only_tasks, resume,
865
- deadline, max_failures, fail_fast, jobs)
866
- _append_spec_reporting(result, reports, started)
867
- return result