@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,77 +0,0 @@
1
- """Shared Markdown primitives: frontmatter splitting and pipe-table headers.
2
-
3
- Pure parsing only — no I/O, no schema knowledge. Feature modules keep their own
4
- field aliases and value semantics.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import re
10
- from typing import Mapping, Optional, Sequence
11
-
12
-
13
- _FRONTMATTER_RE = re.compile(r"\A---[ \t]*\n(.*?)\n---[ \t]*\n?", re.DOTALL)
14
- _SEPARATOR_CELL_RE = re.compile(r"^:?-{2,}:?$")
15
-
16
-
17
- def parse_frontmatter(text: str) -> tuple[Mapping[str, object], str]:
18
- """Split optional frontmatter into flat metadata + body.
19
-
20
- Parses only flat `key: value` string pairs — enough for `description:` —
21
- so the hook hot path never needs pyyaml for this. Content without
22
- frontmatter returns ({}, text) unchanged.
23
- """
24
- if not isinstance(text, str):
25
- return {}, text
26
- match = _FRONTMATTER_RE.match(text)
27
- if match is None:
28
- return {}, text
29
- meta: dict[str, object] = {}
30
- for line in match.group(1).splitlines():
31
- key, sep, value = line.partition(":")
32
- if not sep:
33
- continue
34
- key = key.strip()
35
- if key:
36
- meta[key] = value.strip().strip('"').strip("'")
37
- return meta, text[match.end():]
38
-
39
-
40
- def frontmatter_block(text: str) -> Optional[str]:
41
- """Return the raw frontmatter text (no delimiters), or None when absent."""
42
- if not isinstance(text, str):
43
- return None
44
- match = _FRONTMATTER_RE.match(text)
45
- return match.group(1) if match else None
46
-
47
-
48
- def table_cells(line: str) -> tuple[str, ...]:
49
- """Split one pipe-table line into trimmed cells; () for non-table lines."""
50
- stripped = line.strip()
51
- if not stripped.startswith("|"):
52
- return ()
53
- return tuple(cell.strip() for cell in stripped.strip("|").split("|"))
54
-
55
-
56
- def is_separator_row(cells: Sequence[str]) -> bool:
57
- """True for the `|---|:--:|` alignment row."""
58
- return bool(cells) and all(_SEPARATOR_CELL_RE.match(cell.replace(" ", "")) for cell in cells)
59
-
60
-
61
- def table_columns(lines: Sequence[str]) -> Mapping[str, int]:
62
- """Map header names to column offsets; {} when no header row exists."""
63
- for line in lines:
64
- cells = table_cells(line)
65
- if not cells or is_separator_row(cells):
66
- continue
67
- return {cell: index for index, cell in enumerate(cells)}
68
- return {}
69
-
70
-
71
- def column_index(header: Sequence[str], *names: str) -> int:
72
- """Lowest header index containing any alias (case-insensitive), else -1."""
73
- for index, cell in enumerate(header):
74
- lowered = cell.lower()
75
- if any(name.lower() in lowered for name in names):
76
- return index
77
- return -1
@@ -1,26 +0,0 @@
1
- """Native runtime project discovery and all-or-nothing path validation."""
2
- import os
3
- from pathlib import Path
4
- from codeflow.core.cf_patch import FileOperation
5
-
6
-
7
- def project_root(cwd: str) -> str:
8
- directory = Path(cwd).absolute()
9
- for candidate in (directory, *directory.parents):
10
- if (candidate / ".code-flow/config.yml").is_file():
11
- return str(candidate)
12
- return ""
13
-
14
-
15
- def validated_paths(root: str, cwd: str, operations: tuple[FileOperation, ...]) -> tuple[tuple[str, bool], ...]:
16
- paths: dict[str, bool] = {}
17
- for operation in operations:
18
- for path, deleted in ((operation.path, operation.kind in {"delete", "move"}),
19
- (operation.destination, False)):
20
- if not path:
21
- continue
22
- absolute = Path(os.path.abspath(os.path.join(cwd, path)))
23
- if not absolute.is_relative_to(Path(root)) or not absolute.resolve().is_relative_to(Path(root).resolve()):
24
- raise ValueError(f"patch path is outside project: {path}")
25
- paths[str(absolute)] = deleted
26
- return tuple(paths.items())
@@ -1,33 +0,0 @@
1
- """Shared structured patch parser for native platform entries."""
2
- from dataclasses import dataclass, replace
3
-
4
-
5
- @dataclass(frozen=True)
6
- class FileOperation:
7
- kind: str
8
- path: str
9
- destination: str = ""
10
-
11
-
12
- def parse_patch(command: str) -> tuple[FileOperation, ...]:
13
- lines = command.splitlines()
14
- if len(lines) < 3 or lines[0] != "*** Begin Patch" or lines[-1] != "*** End Patch":
15
- raise ValueError("patch must contain a complete Begin/End envelope")
16
- operations: list[FileOperation] = []
17
- headers = {"*** Add File: ": "add", "*** Update File: ": "update", "*** Delete File: ": "delete"}
18
- for line in lines[1:-1]:
19
- header = next((prefix for prefix in headers if line.startswith(prefix)), "")
20
- if header:
21
- path = line[len(header):]
22
- if not path or "\x00" in path:
23
- raise ValueError("patch file path is empty or invalid")
24
- operations.append(FileOperation(headers[header], path))
25
- elif line.startswith("*** Move to: "):
26
- if not operations or operations[-1].kind != "update" or not line[13:]:
27
- raise ValueError("Move to must follow an Update File")
28
- operations[-1] = replace(operations[-1], kind="move", destination=line[13:])
29
- elif not operations or (line and not line.startswith(("+", "-", " ", "@@", "*** End of File"))):
30
- raise ValueError("unrecognized patch syntax")
31
- if not operations:
32
- raise ValueError("patch contains no file operations")
33
- return tuple(operations)
@@ -1,32 +0,0 @@
1
- """Integrity check for the installed runtime contract and managed artifacts."""
2
- import hashlib
3
- import json
4
- from pathlib import Path
5
-
6
-
7
- def verify_install(root: str) -> None:
8
- directory = Path(root)
9
- if (directory / ".code-flow/.runtime-migration.lock").exists():
10
- raise ValueError("runtime migration is in progress or needs recovery")
11
- manifest = json.loads((directory / ".code-flow/.runtime-install.json").read_text(encoding="utf-8"))
12
- version = (directory / ".code-flow/.version").read_text(encoding="utf-8").strip()
13
- if manifest.get("schema_version") != 1 or manifest.get("version") != version:
14
- raise ValueError("runtime installation version is inconsistent")
15
- files = manifest.get("files")
16
- if not isinstance(files, dict) or not files:
17
- raise ValueError("runtime installation manifest has no artifacts")
18
- for relative, expected in files.items():
19
- path = directory / relative
20
- if not path.resolve().is_relative_to(directory.resolve()):
21
- raise ValueError(f"installation artifact escapes project: {relative}")
22
- if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
23
- raise ValueError(f"managed runtime artifact has drifted: {relative}")
24
- for relative, expected in manifest.get("hook_contracts", {}).items():
25
- actual = json.loads((directory / relative).read_text(encoding="utf-8")).get("hooks", {})
26
- for event, groups in expected.items():
27
- for group in groups:
28
- candidates = [item for item in actual.get(event, [])
29
- if item.get("matcher", "") == group.get("matcher", "")]
30
- for hook in group["hooks"]:
31
- if not any(hook in item.get("hooks", []) for item in candidates):
32
- raise ValueError(f"managed hook has drifted: {relative}:{event}")
@@ -1,37 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Shared per-session state for injection/reminder cadence.
3
-
4
- Stores one JSON file per project root (`.code-flow/.session-state.json`),
5
- keyed by session id. Both read and write are best-effort: a corrupt or
6
- unwritable state degrades to no-op rather than breaking hook protocols.
7
- """
8
-
9
- import json
10
- import os
11
- from pathlib import Path
12
- from typing import Mapping
13
-
14
-
15
- _STATE_PATH = ".code-flow/.session-state.json"
16
-
17
-
18
- def load_session_state(root: str) -> dict:
19
- """Return the persisted state dict; {} on any failure (never raises)."""
20
- try:
21
- value = json.loads((Path(root) / _STATE_PATH).read_text(encoding="utf-8"))
22
- except (OSError, json.JSONDecodeError, UnicodeError):
23
- return {}
24
- return value if isinstance(value, dict) else {}
25
-
26
-
27
- def save_session_state(root: str, state: Mapping) -> bool:
28
- """Atomically persist state; False on failure (never raises)."""
29
- path = Path(root) / _STATE_PATH
30
- try:
31
- path.parent.mkdir(parents=True, exist_ok=True)
32
- temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
33
- temporary.write_text(json.dumps(dict(state), ensure_ascii=False), encoding="utf-8")
34
- os.replace(temporary, path)
35
- return True
36
- except (OSError, UnicodeError, TypeError):
37
- return False
@@ -1,53 +0,0 @@
1
- """Nonblocking process lock for Stop gates; the OS releases abandoned locks."""
2
- from __future__ import annotations
3
-
4
- from contextlib import contextmanager
5
- import json
6
- import os
7
- from pathlib import Path
8
- from typing import IO, Iterator
9
-
10
-
11
- class StopAlreadyRunning(ValueError):
12
- pass
13
-
14
-
15
- def _lock(handle: IO[str]) -> None:
16
- if os.name == "nt":
17
- import msvcrt
18
- handle.seek(0)
19
- msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
20
- else:
21
- import fcntl
22
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
23
-
24
-
25
- def _unlock(handle: IO[str]) -> None:
26
- if os.name == "nt":
27
- import msvcrt
28
- handle.seek(0)
29
- msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
30
- else:
31
- import fcntl
32
- fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
33
-
34
-
35
- @contextmanager
36
- def stop_guard(root: str, sid: str) -> Iterator[None]:
37
- path = Path(root) / ".code-flow/.stop.lock"
38
- path.parent.mkdir(parents=True, exist_ok=True)
39
- with path.open("a+", encoding="utf-8") as handle:
40
- try:
41
- _lock(handle)
42
- except OSError as exc:
43
- handle.seek(0)
44
- owner = handle.read(512).strip() or "owner unavailable"
45
- raise StopAlreadyRunning(f"stop_already_running: {owner}; 等待该进程结束后重试 Stop") from exc
46
- try:
47
- handle.seek(0)
48
- handle.truncate()
49
- json.dump({"pid": os.getpid(), "session_id": sid}, handle)
50
- handle.flush()
51
- yield
52
- finally:
53
- _unlock(handle)
@@ -1,9 +0,0 @@
1
- """Shared lifecycle predicate without loading the workflow runtime.
2
-
3
- 0.8 adds an explicit provisional state: a TASK whose required code checks were
4
- not actually executed must never look finished. Keep the finished set narrow so
5
- dependency resolution, parallel collect and archive cannot treat provisional
6
- work as complete.
7
- """
8
-
9
- FINISHED_STATUSES = ("done", "verified")
@@ -1,408 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Validator engine: parse validation.yml, match triggers, execute commands.
3
-
4
- L1 domain module. It owns the single interpretation of `.code-flow/validation.yml`
5
- (four states: missing / empty / valid / parse_error), trigger matching,
6
- deadline-bound execution, and the content-fingerprint result cache. Consumers:
7
- `cf_validation` (validate), `cf_task_runtime` (finish), `cf_stop_hook` (Stop),
8
- and `cf_task_workflow` (layer counts). Dependencies stay in L0/L1: `cf_core`,
9
- `cf_exec_base`, and `cf_log`.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- from dataclasses import dataclass, replace
15
- from datetime import datetime, timezone
16
- import fnmatch
17
- import hashlib
18
- import json
19
- import math
20
- import os
21
- from pathlib import Path
22
- import re
23
- import time
24
- from typing import Mapping, NamedTuple, Optional, Sequence
25
-
26
- from codeflow.core import cf_log
27
- from codeflow.core.cf_core import normalize_path
28
- from codeflow.core.cf_exec_base import (build_argv, record_report, redact_sensitive_text, report_requested,
29
- remaining_seconds, run_command, worktree_fingerprint)
30
-
31
- MISSING = "missing"
32
- EMPTY = "empty"
33
- VALID = "valid"
34
- PARSE_ERROR = "parse_error"
35
-
36
- VALIDATION_CACHE_FILE = ".validation-cache.json"
37
- MAX_VALIDATION_CACHE_ENTRIES = 256
38
- _BRACE_RE = re.compile(r"\{([^{}]+)\}")
39
-
40
-
41
- @dataclass(frozen=True)
42
- class ValidatorConfig:
43
- """Single interpretation of .code-flow/validation.yml."""
44
-
45
- state: str
46
- validators: tuple[Mapping[str, object], ...] = ()
47
- error: str = ""
48
- path: str = ""
49
-
50
- def without_heavy(self) -> "ValidatorConfig":
51
- """Return a config with only lightweight validators (Stop policy)."""
52
- return replace(self, validators=tuple(item for item in self.validators if not is_heavy(item)))
53
-
54
-
55
- class ValidatorRun(NamedTuple):
56
- failures: list[dict[str, object]]
57
- truncated: bool
58
- reused: int
59
-
60
-
61
- def expand_braces(pattern: str) -> list:
62
- """Expand one level of `{a,b}` alternation → list of fnmatch patterns."""
63
- match = _BRACE_RE.search(pattern)
64
- if not match:
65
- return [pattern]
66
- head, tail = pattern[: match.start()], pattern[match.end():]
67
- out = []
68
- for option in match.group(1).split(","):
69
- out.extend(expand_braces(head + option + tail))
70
- return out
71
-
72
-
73
- def trigger_matches(trigger: str, rel_path: str) -> bool:
74
- """fnmatch with brace expansion; `**/` prefix also matches root files."""
75
- rel_path = normalize_path(rel_path)
76
- for pattern in expand_braces(trigger or ""):
77
- candidates = [pattern]
78
- if pattern.startswith("**/"):
79
- candidates.append(pattern[3:])
80
- if any(fnmatch.fnmatch(rel_path, candidate) for candidate in candidates):
81
- return True
82
- return False
83
-
84
-
85
- def _parse_error(path: str, message: str) -> ValidatorConfig:
86
- cf_log.warn(f"{path}: {message}")
87
- return ValidatorConfig(PARSE_ERROR, error=message, path=path)
88
-
89
-
90
- def _classify(path: str, data: object) -> ValidatorConfig:
91
- if data is None or data == {}:
92
- return ValidatorConfig(EMPTY, path=path)
93
- if not isinstance(data, dict):
94
- return _parse_error(path, "validation.yml must be a YAML mapping")
95
- validators = data.get("validators")
96
- if validators is None or validators == []:
97
- return ValidatorConfig(EMPTY, path=path)
98
- if not isinstance(validators, list):
99
- return _parse_error(path, "validators must be a list")
100
- for index, item in enumerate(validators, 1):
101
- if not isinstance(item, Mapping):
102
- return _parse_error(path, f"validator #{index} must be a mapping")
103
- return ValidatorConfig(VALID, tuple(validators), path=path)
104
-
105
-
106
- def load_validators(project_root: str) -> ValidatorConfig:
107
- """Read validation.yml into one of four explicit states (API-01)."""
108
- path = os.path.join(project_root, ".code-flow", "validation.yml")
109
- if not os.path.exists(path):
110
- return ValidatorConfig(MISSING, path=path)
111
- try:
112
- import yaml
113
- except ImportError as exc:
114
- return _parse_error(path, f"PyYAML unavailable: {exc}")
115
- try:
116
- with open(path, "r", encoding="utf-8") as file:
117
- data = yaml.safe_load(file)
118
- except (OSError, UnicodeError, yaml.YAMLError) as exc:
119
- return _parse_error(path, str(exc))
120
- return _classify(path, data)
121
-
122
-
123
- def package_script_validators(project_root: str) -> tuple[Mapping[str, object], ...]:
124
- """Existing product policy for a missing/empty validation.yml."""
125
- package = Path(project_root) / "package.json"
126
- if not package.is_file():
127
- return ()
128
- data = json.loads(package.read_text(encoding="utf-8"))
129
- scripts = data.get("scripts", {}) if isinstance(data, dict) else {}
130
- if not isinstance(scripts, dict):
131
- raise ValueError("invalid package scripts")
132
- return tuple(
133
- {"name": name, "trigger": "*", "command": f"npm run {name}", "timeout": 60000}
134
- for name in ("test", "lint") if isinstance(scripts.get(name), str)
135
- )
136
-
137
-
138
- def resolve_validators(
139
- project_root: str,
140
- ) -> tuple[ValidatorConfig, tuple[Mapping[str, object], ...]]:
141
- """Config state plus the effective validators for this project.
142
-
143
- missing/empty keep the product policy (package.json test/lint fallback);
144
- parse_error returns no validators so callers must fail closed on state.
145
- """
146
- config = load_validators(project_root)
147
- if config.state == VALID:
148
- return config, config.validators
149
- if config.state == PARSE_ERROR:
150
- return config, ()
151
- return config, package_script_validators(project_root)
152
-
153
-
154
- def is_heavy(validator: object) -> bool:
155
- return isinstance(validator, Mapping) and validator.get("heavy") is True
156
-
157
-
158
- def _validator_failure(validator: Mapping[str, object], detail: str,
159
- incomplete: bool = False) -> dict[str, object]:
160
- result: dict[str, object] = {
161
- "name": validator.get("name", "validator"),
162
- "on_fail": validator.get("on_fail", ""),
163
- "detail": detail,
164
- }
165
- if incomplete:
166
- result["incomplete"] = True
167
- return result
168
-
169
-
170
- def _validator_argv(root: str, validator: Mapping[str, object], matched: list,
171
- strict: bool) -> list:
172
- existing = [name for name in matched if os.path.isfile(os.path.join(root, name))]
173
- template = str(validator.get("command", ""))
174
- if "{files}" in template and not existing:
175
- return []
176
- argv = build_argv(template, existing)
177
- if not argv and strict:
178
- raise ValueError("validator command missing")
179
- return argv
180
-
181
-
182
- def _tool_missing(detail: str) -> bool:
183
- """Recognize 'tool not installed' output so template validators (mypy, npx …)
184
- do not block projects that have not installed the optional tool yet."""
185
- lowered = detail.lower()
186
- markers = (
187
- "no module named",
188
- "modulenotfounderror",
189
- "command not found",
190
- "is not recognized as an internal or external command",
191
- "could not determine executable to run",
192
- "not found in $path",
193
- )
194
- return any(marker in lowered for marker in markers)
195
-
196
-
197
- def _validator_result(root: str, validator: Mapping[str, object], argv: list, sid: str,
198
- deadline: Optional[float], strict: bool,
199
- timings: Optional[dict[str, float]] = None) -> tuple[list, bool, bool]:
200
- try:
201
- timeout = float(validator.get("timeout", 30000)) / 1000.0
202
- except (ValueError, TypeError):
203
- if strict:
204
- return [_validator_failure(validator, "invalid validator timeout")], False, False
205
- timeout = 30.0
206
- if not math.isfinite(timeout) or timeout <= 0:
207
- return [_validator_failure(validator, "invalid validator timeout")], False, False
208
- outcome = run_command(argv, root, timeout, deadline)
209
- if timings is not None:
210
- timings["execution_ms"] += float(outcome.get("execution_ms", 0))
211
- reused = bool(outcome.get("execution_reused"))
212
- passed = outcome["status"] == "ok" and outcome["returncode"] == 0
213
- if outcome["status"] == "deadline_exceeded":
214
- return [_validator_failure(validator, "预算耗尽未执行", True)], True, reused
215
- timed_out = outcome["status"] == "timeout"
216
- detail = (f"超时(>{timeout:.0f}s),未完成" if timed_out else
217
- (str(outcome.get("stdout", "")) + str(outcome.get("stderr", ""))).strip()[-400:])
218
- if outcome["status"] == "spawn_error":
219
- if validator.get("skip_if_missing") is True:
220
- cf_log.degrade(root, "stop_check", f"{validator.get('name')}:tool_missing", sid)
221
- return [], False, reused
222
- cf_log.degrade(root, "stop_check", f"{validator.get('name')}:{outcome['stderr']}", sid)
223
- if not strict:
224
- return [], False, reused
225
- elif not passed and not timed_out and validator.get("skip_if_missing") is True and _tool_missing(detail):
226
- cf_log.degrade(root, "stop_check", f"{validator.get('name')}:tool_missing", sid)
227
- return [], False, reused
228
- cf_log.append_event(root, "stop_check",
229
- {"trigger": validator.get("trigger", ""),
230
- "cmd": Path(argv[0]).name, "passed": passed}, sid)
231
- return ([] if passed else [_validator_failure(validator, redact_sensitive_text(detail))]), timed_out, reused
232
-
233
-
234
- def _tree_fingerprint(project_root: str, validators: Sequence[object]) -> str:
235
- """工作树内容指纹(内容寻址,忽略 .code-flow),与提交状态无关。
236
-
237
- 已提交内容取 blob SHA,未提交/未跟踪内容用工作区文件哈希补齐;未跟踪文件
238
- 只纳入命中任一 validator trigger 的路径(构建/测试产物不使缓存失效)。
239
- 同一内容在提交前后得到相同指纹;返回 "" 表示无法判定(禁用缓存)。
240
- """
241
- triggers = [str(item.get("trigger", "")) for item in validators if isinstance(item, Mapping)]
242
- if triggers:
243
- untracked_filter = lambda path: any(trigger_matches(trigger, path) for trigger in triggers)
244
- else:
245
- untracked_filter = None
246
- return worktree_fingerprint(project_root, untracked_filter)
247
-
248
-
249
- def _validation_cache_path(project_root: str) -> Path:
250
- return Path(project_root) / ".code-flow" / VALIDATION_CACHE_FILE
251
-
252
-
253
- def _load_validation_cache(project_root: str) -> dict:
254
- try:
255
- data = json.loads(_validation_cache_path(project_root).read_text(encoding="utf-8"))
256
- except (OSError, ValueError):
257
- return {}
258
- return data if isinstance(data, dict) else {}
259
-
260
-
261
- def _save_validation_cache(project_root: str, cache: dict) -> None:
262
- if len(cache) > MAX_VALIDATION_CACHE_ENTRIES:
263
- ordered = sorted(
264
- cache.items(),
265
- key=lambda item: str(item[1].get("at", "")) if isinstance(item[1], dict) else "",
266
- )
267
- cache = dict(ordered[-MAX_VALIDATION_CACHE_ENTRIES:])
268
- try:
269
- path = _validation_cache_path(project_root)
270
- path.parent.mkdir(parents=True, exist_ok=True)
271
- path.write_text(json.dumps(cache, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
272
- except OSError:
273
- return # 缓存写入失败不影响验证结论
274
-
275
-
276
- def _cache_key(argv: Sequence[str]) -> str:
277
- return hashlib.sha256(json.dumps({"argv": list(argv)}, ensure_ascii=False).encode()).hexdigest()
278
-
279
-
280
- _CACHE_ENV_KEYS = frozenset({
281
- "DATABASE_URL", "REDIS_URL", "INTERNAL_SERVICE_TOKEN", "ENV", "LOG_DIR", "LOG_LEVEL",
282
- "ARTIFACT_ROOT", "SKILL_CACHE_ROOT", "MIGRATIONS_DIR", "DEFAULT_TENANT_ID", "HOME",
283
- "TMPDIR", "PYTHONPATH", "PYTHONHASHSEED", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "CI",
284
- })
285
-
286
- _CACHE_ENV_PREFIXES = (
287
- "MUAD_", "DATABASE_", "REDIS_", "POSTGRES", "INTERNAL_", "ARTIFACT_", "SKILL_",
288
- "MODEL_", "CAPACITY_", "WECOM_", "CHANNEL_", "GATEWAY_", "RUNTIME_", "WORKER_",
289
- )
290
-
291
-
292
- def _execution_environment() -> dict[str, str]:
293
- """Stable, test-relevant environment fingerprint.
294
-
295
- Shell/terminal bookkeeping (SHLVL, PATH, UV_*, PWD, ...) differs between a
296
- direct invocation and the `bash -lc` wrapper used by spec verifiers without
297
- changing test inputs, so only known test-relevant keys/prefixes participate.
298
- """
299
- return {
300
- key: value for key, value in os.environ.items()
301
- if key in _CACHE_ENV_KEYS or key.startswith(_CACHE_ENV_PREFIXES)
302
- }
303
-
304
-
305
- def _execution_observation(root: str, validator: Mapping[str, object], argv: Sequence[str],
306
- fingerprint: str, entry: object) -> dict[str, str]:
307
- """Describe this execution without exposing commands or environment values."""
308
- payload = {"argv": list(argv), "cwd": str(Path(root).resolve()), "validator": dict(validator),
309
- "environment": _execution_environment(), "input_fingerprint": fingerprint}
310
- identity = hashlib.sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False,
311
- default=str).encode()).hexdigest()
312
- prior = entry.get("fingerprint") if isinstance(entry, dict) else None
313
- previous_identity = entry.get("execution_identity") if isinstance(entry, dict) else None
314
- category = ("unproven" if not fingerprint else "no_baseline" if prior is None else
315
- "worktree_changed" if prior != fingerprint else
316
- "execution_context_changed" if previous_identity != identity else "unchanged")
317
- return {"identity": identity, "input_change_category": category}
318
-
319
-
320
- def _run_one(project_root: str, validator: object, files: Sequence[str], sid: str,
321
- deadline: Optional[float], strict: bool, fingerprint: str,
322
- cache_data: dict, dirty: list[bool], timings: Optional[dict[str, float]] = None,
323
- observations: Optional[list[dict[str, str]]] = None) -> ValidatorRun:
324
- if not isinstance(validator, Mapping):
325
- failures = [_validator_failure({}, "invalid validator record")] if strict else []
326
- return ValidatorRun(failures, False, 0)
327
- matched = [path for path in files if trigger_matches(validator.get("trigger", ""), path)]
328
- if not matched:
329
- return ValidatorRun([], False, 0)
330
- remaining = remaining_seconds(deadline)
331
- if remaining is not None and remaining <= 0:
332
- return ValidatorRun([_validator_failure(validator, "预算耗尽未执行", True)], True, 0)
333
- try:
334
- argv = _validator_argv(project_root, validator, matched, strict)
335
- except ValueError as exc:
336
- return ValidatorRun([_validator_failure(validator, f"validator 配置错误: {exc}")], False, 0)
337
- if not argv:
338
- return ValidatorRun([], False, 0)
339
- key = _cache_key(argv)
340
- entry = cache_data.get(key)
341
- observation = _execution_observation(project_root, validator, argv, fingerprint, entry)
342
- if observations is not None:
343
- observations.append(observation)
344
- if fingerprint:
345
- if (isinstance(entry, dict) and entry.get("fingerprint") == fingerprint
346
- and entry.get("execution_identity") == observation["identity"]):
347
- return ValidatorRun([], False, 1)
348
- errors, incomplete, reused = _validator_result(project_root, validator, argv, sid, deadline, strict, timings)
349
- if fingerprint and not errors and not incomplete:
350
- cache_data[key] = {
351
- "fingerprint": fingerprint,
352
- "at": datetime.now(timezone.utc).isoformat(),
353
- }
354
- cache_data[key]["execution_identity"] = observation["identity"]
355
- dirty[0] = True
356
- return ValidatorRun(errors, incomplete, int(reused))
357
-
358
-
359
- def run_validators(
360
- project_root: str,
361
- config: ValidatorConfig,
362
- files: Sequence[str],
363
- sid: str,
364
- deadline: Optional[float] = None,
365
- *,
366
- strict: bool = False,
367
- cache: bool = True,
368
- timings: Optional[dict[str, float]] = None,
369
- observations: Optional[list[dict[str, str]]] = None,
370
- ) -> ValidatorRun:
371
- """Match validators, execute argv under one deadline, report unrun work.
372
-
373
- `cache` reuses passing results when the worktree content fingerprint and
374
- argv are unchanged (failures are never cached). Returns (failures,
375
- truncated, reused).
376
- """
377
- validators = list(config.validators)
378
- if report_requested():
379
- if timings is None:
380
- timings = {"planning_ms": 0.0, "preflight_ms": 0.0, "execution_ms": 0.0, "evidence_ms": 0.0}
381
- if observations is None:
382
- observations = []
383
- started = time.monotonic()
384
- fingerprint = _tree_fingerprint(project_root, validators) if cache and validators else ""
385
- cache_data = _load_validation_cache(project_root) if fingerprint else {}
386
- if timings is not None:
387
- timings["preflight_ms"] += (time.monotonic() - started) * 1000
388
- dirty = [False]
389
- failures: list[dict[str, object]] = []
390
- truncated = False
391
- reused = 0
392
- for validator in validators:
393
- outcome = _run_one(project_root, validator, files, sid, deadline, strict,
394
- fingerprint, cache_data, dirty, timings, observations)
395
- failures.extend(outcome.failures)
396
- truncated = truncated or outcome.truncated
397
- reused += outcome.reused
398
- if dirty[0]:
399
- started = time.monotonic()
400
- _save_validation_cache(project_root, cache_data)
401
- if timings is not None:
402
- timings["evidence_ms"] += (time.monotonic() - started) * 1000
403
- if report_requested():
404
- record_report({"kind": "validators", "scope": {"files": list(files)}, "timings": dict(timings or {}),
405
- "execution_identities": [item["identity"] for item in observations or []],
406
- "input_change_category": [item["input_change_category"] for item in observations or []],
407
- "reused": reused, "selected": len(observations or [])})
408
- return ValidatorRun(failures, truncated, reused)