@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,10 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Path-frozen entry shim: implementation lives in codeflow.cli.cf_validation."""
3
- import sys
4
-
5
- from codeflow.cli import cf_validation as _impl
6
-
7
- if __name__ == "__main__":
8
- raise SystemExit(_impl.main())
9
- else:
10
- sys.modules[__name__] = _impl
File without changes
@@ -1,347 +0,0 @@
1
- """Append execution history without erasing the user's acceptance contract."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import re
6
- from datetime import datetime, timezone
7
- from pathlib import Path
8
- from typing import Mapping, Optional, Sequence
9
- from uuid import uuid4
10
-
11
- from codeflow.evidence.cf_evidence_store import EvidenceStore, discover_project_root, git_commit
12
- from codeflow.acceptance.cf_acceptance_schema import check_outcome, load_manifest, recorded_check_hash, scenario_check_ids
13
-
14
- STATES = frozenset(("planned", "pending", "tbd", "verified", "unverified", "stale", "failed", "incomplete", "not_configured", "e2e_deferred", "env_blocked", "manual_pending", "accepted_risk"))
15
- _COVERAGE_STATES = STATES | {"designed"}
16
- _EVIDENCE_STATUS = {
17
- "passed": "passed",
18
- "failed": "failed",
19
- "incomplete": "incomplete",
20
- "not_configured": "not_configured",
21
- "e2e_deferred": "deferred",
22
- "env_blocked": "env_blocked",
23
- "accepted_risk": "accepted_risk",
24
- }
25
-
26
-
27
- def scenario_line(line: str, scenario_id: str) -> bool:
28
- return bool(re.match(rf"^\s*(?:\|\s*|[-*]\s+){re.escape(scenario_id)}(?=\s*[:|])", line))
29
-
30
-
31
- def line_status(line: str, scenario_id: str) -> str:
32
- if not scenario_line(line, scenario_id):
33
- return ""
34
- if line.lstrip().startswith("|"):
35
- cells = [cell.strip().lower() for cell in line.strip().strip("|").split("|")]
36
- return next((cell for cell in reversed(cells) if cell in _COVERAGE_STATES), "")
37
- match = re.match(r"^\s*[-*]\s+[^:]+:\s*([a-z0-9_]+)\b", line, re.I)
38
- return match.group(1).lower() if match and match.group(1).lower() in _COVERAGE_STATES else ""
39
-
40
-
41
- def _status_index(header: str) -> int:
42
- cells = [cell.strip().lower() for cell in header.strip().strip("|").split("|")]
43
- for index, cell in enumerate(cells):
44
- if cell in ("状态", "status"):
45
- return index + 1
46
- return -1
47
-
48
-
49
- def _status_line(line: str, scenario_id: str, status: str, status_index: int = -1) -> str:
50
- if not scenario_line(line, scenario_id):
51
- return line
52
- if line.lstrip().startswith("|"):
53
- cells = line.split("|")
54
- if 0 < status_index < len(cells) - 1:
55
- cells[status_index] = f" {status} "
56
- return "|".join(cells)
57
- for index in range(len(cells) - 1, 0, -1):
58
- if cells[index].strip().lower() in _COVERAGE_STATES:
59
- cells[index] = f" {status} "
60
- return "|".join(cells)
61
- return line.rstrip().rstrip("|") + f"| {status} |"
62
- prefix = r"(^\s*[-*]\s+[^:]+:\s*)"
63
- if line_status(line, scenario_id):
64
- return re.sub(prefix + r"(?:" + "|".join(sorted(_COVERAGE_STATES)) + r")\b", rf"\g<1>{status}", line, count=1, flags=re.I)
65
- return re.sub(prefix, rf"\g<1>{status} — ", line, count=1)
66
-
67
-
68
- def _patch_section(section: str, scenario_id: str, status: str, record: str) -> str:
69
- contract = re.search(r"(?ms)^### Acceptance Contract[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
70
- if contract:
71
- lines = contract.group(1).splitlines()
72
- header = next((line for line in lines if line.lstrip().startswith("|")
73
- and ("状态" in line or "status" in line.lower())), "")
74
- index = _status_index(header)
75
- body = "\n".join(_status_line(line, scenario_id, status, index) for line in lines) + "\n"
76
- section = section[:contract.start(1)] + body + section[contract.end(1):]
77
- evidence = re.search(r"(?ms)^### Acceptance Evidence[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
78
- if evidence:
79
- body = evidence.group(1)
80
- if record not in body.splitlines():
81
- body = body.rstrip() + "\n" + record + "\n\n"
82
- section = section[:evidence.start(1)] + body + section[evidence.end(1):]
83
- else:
84
- section = section.rstrip() + "\n\n### Acceptance Evidence\n" + record + "\n"
85
- return section
86
-
87
-
88
- def sync_task_evidence(task: Path, scenario_id: str, confirmed_by: str, evidence: str, owner: str = "", status: str = "verified") -> str:
89
- """Sync one scenario's status into the task file; returns what was written.
90
-
91
- - owner + TASK section → contract/evidence + coverage
92
- - ownerless legacy scenario → coverage only (a scenario with neither owner
93
- nor coverage has no truthful place to attribute evidence; first-TASK
94
- attribution corrupted unrelated contracts, so it is skipped)
95
- """
96
- text = task.read_text(encoding="utf-8")
97
- record = f"- {scenario_id}: {status} — {evidence} (confirmed_by: {confirmed_by})"
98
- if owner:
99
- task_pattern = re.escape(owner)
100
- match = re.search(rf"(?ms)^##\s+{task_pattern}:.*?(?=^## |\Z)", text)
101
- if match is None:
102
- raise ValueError(f"acceptance owner missing: {owner}")
103
- section = _patch_section(match.group(0), scenario_id, status, record)
104
- text = text[:match.start()] + section + text[match.end():]
105
- coverage = re.search(r"(?ms)^## Acceptance Coverage[^\n]*\n(.*?)(?=^## |\Z)", text)
106
- if coverage:
107
- lines = coverage.group(1).splitlines()
108
- header = next((line for line in lines if line.lstrip().startswith("|")
109
- and ("状态" in line or "status" in line.lower())), "")
110
- index = _status_index(header)
111
- body = "\n".join(_status_line(line, scenario_id, status, index) for line in lines) + "\n"
112
- text = text[:coverage.start(1)] + body + text[coverage.end(1):]
113
- elif not owner:
114
- return "skipped_no_owner"
115
- from codeflow.core.cf_fs import atomic_write_text
116
- atomic_write_text(task, text)
117
- return "synced"
118
-
119
-
120
- def _run_record(row: Mapping[str, object], result: Mapping[str, object], run_id: str,
121
- executed_at: str, fingerprint: str, commit: str,
122
- check_config: Optional[Mapping[str, object]] = None) -> dict[str, object]:
123
- source = check_config if isinstance(check_config, Mapping) else row
124
- command = source.get("command")
125
- if not isinstance(command, list):
126
- command = []
127
- timeout = source.get("timeout", 60)
128
- cwd = source.get("cwd", ".")
129
- state = str(result.get("status"))
130
- exit_code = result.get("exit_code")
131
- return {
132
- "run_id": run_id,
133
- "check_id": str(row.get("check") or row.get("id")),
134
- "scenario_id": str(row.get("id")),
135
- "command": command,
136
- "cwd": cwd if isinstance(cwd, str) else ".",
137
- "timeout": timeout if isinstance(timeout, (int, float)) else 60,
138
- "input_hash": fingerprint if isinstance(fingerprint, str) else "",
139
- "exit_code": exit_code if isinstance(exit_code, int) else (0 if state == "passed" else 1),
140
- "status": _EVIDENCE_STATUS[state],
141
- "executed_at": executed_at,
142
- "source": str(result.get("source") or "local"),
143
- "environment_hash": str(result.get("environment_hash") or ""),
144
- "commit": commit,
145
- "run_url": str(result.get("run_url") or ""),
146
- }
147
-
148
-
149
- def _outcome_mapping(result: Mapping[str, object], field: str) -> Mapping[str, object]:
150
- value = result.get(field)
151
- return value if isinstance(value, Mapping) else {}
152
-
153
-
154
- def _store_evidence_runs(path: Path, rows: Mapping[str, Mapping[str, object]],
155
- results: Sequence[Mapping[str, object]], run_id: str,
156
- executed_at: str, fingerprint: str) -> dict[str, int]:
157
- from codeflow.evidence.cf_evidence_store import requirement_key
158
- project = discover_project_root(path.parent)
159
- store = EvidenceStore(project, requirement_key(path.parent))
160
- commit = git_commit(project)
161
- created = duplicates = 0
162
- for result in results:
163
- state = str(result.get("status"))
164
- if state not in _EVIDENCE_STATUS:
165
- continue
166
- if result.get("cache_reused") is True and result.get("checkpoint_reused") is not True:
167
- continue
168
- row = rows.get(str(result.get("id")))
169
- if row is None:
170
- continue
171
- result_fingerprint = str(result.get("input_hash") or fingerprint)
172
- raw_ids = result.get("check_ids")
173
- check_ids = [str(value) for value in raw_ids] if isinstance(raw_ids, list) else []
174
- check_id = str(row.get("check") or row.get("id"))
175
- if not check_ids:
176
- check_ids = [check_id]
177
- check_hashes = _outcome_mapping(result, "check_hashes")
178
- check_reused = _outcome_mapping(result, "check_reused")
179
- check_checkpoint = _outcome_mapping(result, "checkpoint_reused_checks")
180
- configs = _outcome_mapping(result, "check_configs")
181
- for item_id in check_ids:
182
- checkpointed = check_checkpoint.get(item_id, result.get("checkpoint_reused"))
183
- item_hash = str(check_hashes.get(item_id) or result_fingerprint)
184
- if check_reused.get(item_id) is True:
185
- if checkpointed is not True:
186
- continue
187
- if any(
188
- run.get("input_hash") == item_hash
189
- and run.get("status") in ("passed", "verified")
190
- for run in store.list_runs(item_id)
191
- ):
192
- continue
193
- config = configs.get(item_id)
194
- record = _run_record(
195
- row, check_outcome(result, item_id), run_id, executed_at, item_hash, commit,
196
- config if isinstance(config, Mapping) else None,
197
- )
198
- record["check_id"] = item_id
199
- outcome = store.write_run(item_id, record)
200
- created += outcome["status"] == "created"
201
- duplicates += outcome["status"] == "duplicate"
202
- return {"created": created, "duplicates": duplicates}
203
-
204
-
205
- def _restore_checkpoint(row: dict[str, object], result: Mapping[str, object], run_key: str,
206
- executed_at: str, fingerprint: str) -> Optional[tuple[dict[str, object], str]]:
207
- if result["status"] != "passed" or row.get("status") == "verified":
208
- return None
209
- row["revision"] = int(row.get("revision", 0)) + 1
210
- record: dict[str, object] = {
211
- "id": result["id"],
212
- "kind": str(result.get("kind", "functional")),
213
- "status": "passed",
214
- "exit_code": 0,
215
- "cache_reused": True,
216
- "checkpoint_reused": True,
217
- "revision": row["revision"],
218
- "run_id": run_key,
219
- "executed_at": executed_at,
220
- "command": row.get("command"),
221
- "cwd": row.get("cwd", "."),
222
- "timeout": row.get("timeout", 60),
223
- }
224
- if fingerprint:
225
- record["fingerprint"] = fingerprint
226
- for key in ("check_ids", "check_hashes", "check_results", "check_reused", "checkpoint_reused_checks", "check_configs", "input_hash", "risk"):
227
- if key in result:
228
- record[key] = result[key]
229
- row["status"] = "verified"
230
- row.pop("stale_checks", None)
231
- row.pop("stale_reason", None)
232
- return record, "verified"
233
-
234
-
235
- def _record_result(row: dict[str, object], result: Mapping[str, object], run_key: str,
236
- executed_at: str, fingerprint: str) -> Optional[tuple[dict[str, object], str]]:
237
- """Apply one result to a manifest row; None means keep the row unchanged."""
238
- state = str(result["status"])
239
- if result.get("cache_reused") is True:
240
- return _restore_checkpoint(row, result, run_key, executed_at, fingerprint)
241
- # A deferral placeholder (e2e skipped in a non-E2E run) must never
242
- # downgrade a verified terminal scenario: verify-e2e runs first, then
243
- # later Done/finish passes would otherwise silently un-verify it.
244
- if state == "e2e_deferred" and row.get("status") == "verified":
245
- return None
246
- # An unavailable environment is not new product evidence: keep a prior
247
- # verified conclusion, but never promote an unverified row.
248
- if state == "env_blocked" and row.get("status") == "verified":
249
- return None
250
- row["revision"] = int(row.get("revision", 0)) + 1
251
- status = "verified" if state == "passed" else state
252
- row["status"] = status
253
- row.pop("stale_checks", None)
254
- row.pop("stale_reason", None)
255
- record = {**result, "revision": row["revision"], "run_id": run_key, "executed_at": executed_at}
256
- # Identity for cross-run reuse checks: command/cwd/timeout + content
257
- # fingerprint of the worktree the command actually ran against.
258
- record["command"] = row.get("command")
259
- record["cwd"] = row.get("cwd", ".")
260
- record["timeout"] = row.get("timeout", 60)
261
- if fingerprint and status == "verified":
262
- record["fingerprint"] = fingerprint
263
- return record, status
264
-
265
-
266
- def stale_scenario_checks(row: Mapping[str, object], hashes: Mapping[str, Mapping[str, object]],
267
- fingerprint: str) -> list[str]:
268
- """Compare every declared check; a missing or unavailable hash is stale."""
269
- if row.get("status") not in ("verified", "accepted_risk", "stale"):
270
- return []
271
- if row.get("kind") == "manual" or row.get("exec") in ("manual", "aspirational"):
272
- return []
273
- check_ids = scenario_check_ids(row)
274
- evidence = row.get("evidence")
275
- evidence = evidence if isinstance(evidence, Mapping) else {}
276
- if not check_ids:
277
- return [] if fingerprint and evidence.get("fingerprint") == fingerprint else [str(row["id"])]
278
- stale = []
279
- for check_id in check_ids:
280
- expected = hashes.get(check_id, {}).get("input_hash")
281
- actual = recorded_check_hash(row, check_id)
282
- if not expected or expected != actual:
283
- stale.append(check_id)
284
- return stale
285
-
286
-
287
- def invalidate_manifest_evidence(root: Path, manifest: Path,
288
- fingerprint: str) -> list[dict[str, object]]:
289
- """Mark outdated conclusions without changing historical execution evidence."""
290
- from codeflow.acceptance.cf_check_inputs import check_input_hashes
291
- from codeflow.core.cf_fs import atomic_write_text
292
- data = load_manifest(manifest)
293
- checks = {str(check["id"]): check for check in data.get("checks", [])}
294
- hashes = check_input_hashes(str(root), checks, fingerprint)
295
- changed: list[dict[str, object]] = []
296
- for row in data["scenarios"]:
297
- stale = stale_scenario_checks(row, hashes, fingerprint)
298
- if not stale:
299
- continue
300
- row.update(status="stale", stale_checks=stale)
301
- changed.append({"scenario_id": row["id"], "checks": stale})
302
- if not changed:
303
- return []
304
- atomic_write_text(manifest, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
305
- task_name = data.get("task_file")
306
- if isinstance(task_name, str) and task_name:
307
- task = (manifest.parent / task_name).resolve()
308
- rows = {row["id"]: row for row in data["scenarios"]}
309
- for item in changed:
310
- row = rows[item["scenario_id"]]
311
- sync_task_evidence(task, str(row["id"]), "runtime",
312
- "input changed: " + ", ".join(item["checks"]),
313
- str(row.get("owner", "")), "stale")
314
- return changed
315
-
316
-
317
- def persist_results(path: Path, data: dict[str, object], results: Sequence[Mapping[str, object]],
318
- fingerprint: str = "", run_id: str = "") -> None:
319
- rows = {row["id"]: row for row in data["scenarios"]}
320
- run_key, executed_at = run_id or uuid4().hex, datetime.now(timezone.utc).isoformat()
321
- updates = []
322
- for result in results:
323
- state = str(result["status"])
324
- if state in ("not_included", "manual_pending"):
325
- continue
326
- row = rows[result["id"]]
327
- result_fingerprint = str(result.get("input_hash") or fingerprint)
328
- applied = _record_result(row, result, run_key, executed_at, result_fingerprint)
329
- if applied is None:
330
- continue
331
- record, status = applied
332
- row["evidence"] = record
333
- if result.get("cache_reused") is not True:
334
- runs = row.setdefault("runs", [])
335
- runs.append(record)
336
- if len(runs) > 50:
337
- del runs[:-50]
338
- updates.append((row, record, status))
339
- from codeflow.core.cf_fs import atomic_write_text
340
- atomic_write_text(path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
341
- _store_evidence_runs(path, rows, results, run_key, executed_at, fingerprint)
342
- task_name = data.get("task_file")
343
- if isinstance(task_name, str) and task_name:
344
- task = Path(task_name) if Path(task_name).is_absolute() else path.parent / task_name
345
- for row, record, status in updates:
346
- detail = f"automated command {record['status']}; run_id={run_key}"
347
- sync_task_evidence(task, str(row["id"]), "runner", detail, str(row.get("owner", "")), status)