@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,687 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Rule verifier registry and hash-bound Verification Evidence."""
3
-
4
- from __future__ import annotations
5
-
6
- import ast
7
- from dataclasses import dataclass, replace
8
- from datetime import datetime, timezone
9
- import fnmatch
10
- import hashlib
11
- import json
12
- import os
13
- from pathlib import Path
14
- from threading import Lock
15
- import time
16
- from concurrent.futures import ThreadPoolExecutor
17
- from contextvars import copy_context
18
- from typing import Mapping, Optional, Sequence
19
-
20
- from codeflow.acceptance.cf_checks import run_regex_verifier
21
- from codeflow.spec.cf_spec_metadata import SpecMetadata, SpecRule, SpecVerifier
22
- from codeflow.core.cf_exec_base import execution_session, record_report, redact_sensitive_text, report_requested, run_command
23
- from codeflow.core import cf_git
24
-
25
-
26
- _RESULT_CACHE: dict[str, dict[str, object]] = {}
27
- _RESULT_CACHE_ROOT: Optional[str] = None
28
- _RESULT_CACHE_DIRTY = False
29
- _RESULT_CACHE_LOCK = Lock()
30
- _MAX_RESULT_CACHE_ENTRIES = 2048
31
- _PURE_VERIFIER_TYPES = frozenset(("document", "regex", "ast"))
32
-
33
-
34
- @dataclass(frozen=True)
35
- class VerificationScope:
36
- root: str
37
- files: tuple[str, ...]
38
- diff_sha256: Optional[str]
39
-
40
-
41
- @dataclass(frozen=True)
42
- class VerificationEvidence:
43
- verifier_ref: str
44
- executed_at: str
45
- status: str
46
- rule_text_sha256: str
47
- artifact_sha256: Optional[str]
48
- diff_sha256: Optional[str]
49
- result_sha256: str
50
- error_code: Optional[str]
51
- details: Mapping[str, object]
52
-
53
-
54
- @dataclass(frozen=True)
55
- class VerificationResult:
56
- evidence: tuple[VerificationEvidence, ...]
57
- passed: bool
58
-
59
-
60
- def _cache_file(root: str) -> Path:
61
- return Path(root) / ".code-flow" / ".verifier-cache.json"
62
-
63
-
64
- def _cache_key(metadata: SpecMetadata, rule: SpecRule, verifier: SpecVerifier, scope: VerificationScope, confirmation: Optional[Mapping[str, object]], stage: str = "code") -> str:
65
- payload = {
66
- "spec": metadata.hashes.file_sha256,
67
- "rule": rule.text_sha256,
68
- "verifier": {"type": verifier.type, "config": verifier.config},
69
- "files": scope.files,
70
- "diff": scope.diff_sha256,
71
- "stage": stage,
72
- "confirmation": confirmation or {},
73
- "artifact_content": _artifact_fingerprint(verifier, scope),
74
- }
75
- return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
76
-
77
-
78
- def _normalize_scope_path(value: str) -> str:
79
- return value.replace("\\", "/").strip()
80
-
81
-
82
- def _matches_scope(path: str, patterns: Sequence[str]) -> bool:
83
- for pattern in patterns:
84
- if fnmatch.fnmatchcase(path, pattern):
85
- return True
86
- bare = pattern.rstrip("/")
87
- if not any(char in pattern for char in "*?[") and path.startswith(bare + "/"):
88
- return True
89
- return False
90
-
91
-
92
- def scoped_files(verifier: SpecVerifier, scope: VerificationScope) -> tuple[str, ...]:
93
- """作用域命中的文件集合:git tracked ∪ 任务持有文件,按 verifier.files 过滤。
94
-
95
- 决议仓库级集合(而非仅当前任务 diff)才能让未触及作用域的其他任务命中缓存。
96
- """
97
- if not verifier.files:
98
- return ()
99
- candidates = {_normalize_scope_path(item) for item in scope.files if item.strip()}
100
- candidates.update(cf_git.tracked_files(scope.root))
101
- return tuple(sorted(path for path in candidates if _matches_scope(path, verifier.files)))
102
-
103
-
104
- def _scope_fingerprint(root: str, files: Sequence[str]) -> str:
105
- digest = hashlib.sha256()
106
- for relative in files:
107
- path = Path(root) / relative
108
- digest.update(relative.encode("utf-8"))
109
- try:
110
- digest.update(path.read_bytes() if path.is_file() else b"<deleted>")
111
- except OSError:
112
- digest.update(b"<unreadable>")
113
- return digest.hexdigest()
114
-
115
-
116
- def _scoped_cache_key(
117
- metadata: SpecMetadata,
118
- rule: SpecRule,
119
- verifier: SpecVerifier,
120
- scope: VerificationScope,
121
- confirmation: Optional[Mapping[str, object]],
122
- stage: str,
123
- ) -> Optional[str]:
124
- """command/test 仅在声明 files 作用域时缓存;未声明保持全量执行。"""
125
- if verifier.type not in ("command", "test") or not verifier.files:
126
- return None
127
- files = scoped_files(verifier, scope)
128
- payload = {
129
- "spec": metadata.hashes.file_sha256,
130
- "rule": rule.text_sha256,
131
- "verifier": {"type": verifier.type, "config": verifier.config, "files": list(verifier.files)},
132
- "stage": stage,
133
- "scope_files": files,
134
- "scope_sha256": _scope_fingerprint(scope.root, files),
135
- "confirmation": confirmation or {},
136
- }
137
- return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
138
-
139
-
140
- def _artifact_fingerprint(verifier: SpecVerifier, scope: VerificationScope) -> Optional[str]:
141
- if verifier.type != "document":
142
- return None
143
- relative = verifier.config.get("artifact")
144
- if not isinstance(relative, str):
145
- return "invalid"
146
- try:
147
- return hashlib.sha256((Path(scope.root) / relative).read_bytes()).hexdigest()
148
- except OSError as exc:
149
- return f"unreadable:{exc}"
150
-
151
-
152
- def _load_result_cache(root: str) -> None:
153
- global _RESULT_CACHE_ROOT, _RESULT_CACHE, _RESULT_CACHE_DIRTY
154
- if _RESULT_CACHE_ROOT == root:
155
- return
156
- _RESULT_CACHE_ROOT = root
157
- _RESULT_CACHE = {}
158
- _RESULT_CACHE_DIRTY = False
159
- path = _cache_file(root)
160
- try:
161
- loaded = json.loads(path.read_text(encoding="utf-8"))
162
- if isinstance(loaded, dict):
163
- _RESULT_CACHE.update({str(key): value for key, value in loaded.items() if isinstance(value, dict)})
164
- except (OSError, ValueError):
165
- pass
166
-
167
-
168
- def _cached_evidence(key: str) -> Optional[VerificationEvidence]:
169
- item = _RESULT_CACHE.get(key)
170
- if not isinstance(item, dict) or item.get("status") != "verified":
171
- return None
172
- try:
173
- return VerificationEvidence(**item)
174
- except (TypeError, ValueError):
175
- return None
176
-
177
-
178
- def _save_result_cache(key: str, evidence: VerificationEvidence) -> None:
179
- global _RESULT_CACHE_DIRTY
180
- with _RESULT_CACHE_LOCK:
181
- _RESULT_CACHE[key] = evidence.__dict__
182
- _RESULT_CACHE_DIRTY = True
183
- while len(_RESULT_CACHE) > _MAX_RESULT_CACHE_ENTRIES:
184
- _RESULT_CACHE.pop(next(iter(_RESULT_CACHE)))
185
-
186
-
187
- def _flush_result_cache(root: str) -> None:
188
- global _RESULT_CACHE_DIRTY
189
- with _RESULT_CACHE_LOCK:
190
- if not _RESULT_CACHE_DIRTY:
191
- return
192
- path = _cache_file(root)
193
- try:
194
- path.parent.mkdir(parents=True, exist_ok=True)
195
- temporary = path.with_suffix(".json.tmp")
196
- temporary.write_text(
197
- json.dumps(_RESULT_CACHE, ensure_ascii=False, sort_keys=True, default=str), encoding="utf-8"
198
- )
199
- temporary.replace(path)
200
- _RESULT_CACHE_DIRTY = False
201
- except OSError:
202
- pass
203
-
204
-
205
- def _cached_or_run(
206
- metadata: SpecMetadata,
207
- rule: SpecRule,
208
- verifier: SpecVerifier,
209
- scope: VerificationScope,
210
- confirmation: Optional[Mapping[str, object]],
211
- stage: str = "code",
212
- ) -> VerificationEvidence:
213
- if verifier.type == "regex" and not _regex_scope_files(verifier.config, metadata, scope):
214
- # The fallback checks tracked files outside the diff. Do not reuse a
215
- # diff-keyed result when those files may have changed independently.
216
- return _evidence(metadata, rule, verifier, scope, confirmation)
217
- cacheable = verifier.type in _PURE_VERIFIER_TYPES
218
- scoped_key: Optional[str] = None
219
- if cacheable:
220
- key = _cache_key(metadata, rule, verifier, scope, confirmation, stage)
221
- else:
222
- scoped_key = _scoped_cache_key(metadata, rule, verifier, scope, confirmation, stage)
223
- if scoped_key is None:
224
- key = ""
225
- else:
226
- cacheable, key = True, scoped_key
227
- if cacheable:
228
- cached = _cached_evidence(key)
229
- if cached is not None:
230
- record_report({"kind": "spec_check", "check_id": f"{metadata.id}#{rule.ref}",
231
- "reused": 1, "execution_ms": 0.0})
232
- if scoped_key is None:
233
- return cached
234
- details = dict(cached.details or {})
235
- details["cache_reused"] = True
236
- details["scope_files"] = list(scoped_files(verifier, scope))
237
- return replace(
238
- cached,
239
- executed_at=datetime.now(timezone.utc).isoformat(),
240
- diff_sha256=scope.diff_sha256,
241
- details=details,
242
- )
243
- current = _evidence(metadata, rule, verifier, scope, confirmation)
244
- if cacheable and current.status == "verified":
245
- _save_result_cache(key, current)
246
- return current
247
-
248
-
249
- @dataclass(frozen=True)
250
- class _Outcome:
251
- passed: bool
252
- error_code: Optional[str]
253
- artifact_sha256: Optional[str]
254
- details: Mapping[str, object]
255
- execution_ms: Optional[float] = None
256
- execution_reused: bool = False
257
-
258
-
259
- def _hash_bytes(data: bytes) -> str:
260
- return hashlib.sha256(data).hexdigest()
261
-
262
-
263
- def _result_hash(status: str, error: Optional[str], details: Mapping[str, object]) -> str:
264
- payload = {"status": status, "error_code": error, "details": details}
265
- encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
266
- return _hash_bytes(encoded)
267
-
268
-
269
- def _config_string(config: Mapping[str, object], key: str) -> Optional[str]:
270
- value = config.get(key)
271
- return value if isinstance(value, str) and value else None
272
-
273
-
274
- def _read_scope_files(scope: VerificationScope) -> tuple[dict[str, str], Optional[_Outcome]]:
275
- contents: dict[str, str] = {}
276
- for relative in scope.files:
277
- path = Path(scope.root) / relative
278
- try:
279
- contents[relative] = path.read_text(encoding="utf-8")
280
- except (OSError, UnicodeError) as exc:
281
- outcome = _Outcome(False, "file_read_error", None, {"file": relative, "error": str(exc)})
282
- return {}, outcome
283
- return contents, None
284
-
285
-
286
- def _document(config: Mapping[str, object], scope: VerificationScope) -> _Outcome:
287
- relative = _config_string(config, "artifact")
288
- section = _config_string(config, "section_id")
289
- item = _config_string(config, "item_id")
290
- if not relative or not section or not item:
291
- return _Outcome(False, "invalid_verifier_config", None, {"required": ["artifact", "section_id", "item_id"]})
292
- path = Path(scope.root) / relative
293
- try:
294
- content = path.read_text(encoding="utf-8")
295
- artifact_hash = _hash_bytes(path.read_bytes())
296
- except (OSError, UnicodeError) as exc:
297
- return _Outcome(False, "artifact_read_error", None, {"artifact": relative, "error": str(exc)})
298
- expected = _config_string(config, "artifact_sha256")
299
- from codeflow.spec.cf_rule_readiness import referenced_item, unresolved_text
300
- selected = referenced_item(content, section, item)
301
- present = bool(selected)
302
- hash_matches = expected is None or expected == artifact_hash
303
- unresolved = unresolved_text(selected)
304
- details = {"artifact": relative, "section_present": section in content, "item_present": bool(selected),
305
- "hash_matches": hash_matches, "unresolved": unresolved}
306
- error = "rule_applied_unresolved" if unresolved else "document_mismatch"
307
- passed = present and hash_matches and not unresolved
308
- return _Outcome(passed, None if passed else error, artifact_hash, details)
309
-
310
-
311
- def _regex_file_pattern(config: Mapping[str, object], metadata: SpecMetadata) -> str:
312
- check_id = _config_string(config, "check_id")
313
- check = next((item for item in metadata.checks if item.get("id") == check_id), None)
314
- if check is not None and isinstance(check.get("files"), str):
315
- return str(check["files"])
316
- return _config_string(config, "files") or "*"
317
-
318
-
319
- def _regex_scope_files(config: Mapping[str, object], metadata: SpecMetadata,
320
- scope: VerificationScope) -> tuple[str, ...]:
321
- files = _regex_file_pattern(config, metadata)
322
- return tuple(relative for relative in scope.files
323
- if fnmatch.fnmatch(relative, files) and (Path(scope.root) / relative).is_file())
324
-
325
-
326
- def _regex(config: Mapping[str, object], metadata: SpecMetadata, scope: VerificationScope) -> _Outcome:
327
- pattern = _config_string(config, "pattern")
328
- files = _config_string(config, "files") or "*"
329
- check_id = _config_string(config, "check_id")
330
- if check_id:
331
- check = next((item for item in metadata.checks if item.get("id") == check_id), None)
332
- if check is None:
333
- return _Outcome(False, "check_not_found", None, {"check_id": check_id})
334
- pattern = check.get("pattern") if isinstance(check.get("pattern"), str) else None
335
- files = check.get("files") if isinstance(check.get("files"), str) else "*"
336
- if pattern is None:
337
- return _Outcome(False, "invalid_verifier_config", None, {"required": ["pattern"]})
338
- selected = _regex_scope_files(config, metadata, scope)
339
- if not selected:
340
- tracked = tuple(relative for relative in cf_git.tracked_files(scope.root)
341
- if fnmatch.fnmatch(relative, files))
342
- if any(not (Path(scope.root) / relative).is_file() for relative in tracked):
343
- return _Outcome(False, "no_matching_files", None,
344
- {"checked_files": 0, "files": files, "reason": "tracked_files_missing"})
345
- selected = tracked
346
- if not selected:
347
- return _Outcome(False, "no_matching_files", None, {"checked_files": 0, "files": files})
348
- contents, error = _read_scope_files(VerificationScope(scope.root, selected, scope.diff_sha256))
349
- if error is not None:
350
- return error
351
- violations, skipped = run_regex_verifier(pattern, files, contents)
352
- if skipped:
353
- return _Outcome(False, "regex_unavailable", None, {"skipped": skipped})
354
- details = {"checked_files": len(contents), "violations": violations}
355
- return _Outcome(not violations, None if not violations else "regex_violation", None, details)
356
-
357
-
358
- def _call_name(node: ast.Call) -> str:
359
- if isinstance(node.func, ast.Name):
360
- return node.func.id
361
- if isinstance(node.func, ast.Attribute):
362
- return node.func.attr
363
- return ""
364
-
365
-
366
- def _ast(config: Mapping[str, object], scope: VerificationScope) -> _Outcome:
367
- if _config_string(config, "language") != "python":
368
- return _Outcome(False, "unsupported_ast_language", None, {"language": config.get("language")})
369
- assertion = _config_string(config, "assertion") or "parse"
370
- contents, error = _read_scope_files(scope)
371
- if error is not None:
372
- return error
373
- parsed: list[ast.AST] = []
374
- try:
375
- parsed = [ast.parse(content, filename=relative) for relative, content in contents.items()]
376
- except SyntaxError as exc:
377
- return _Outcome(False, "ast_parse_error", None, {"error": str(exc)})
378
- if assertion == "parse":
379
- return _Outcome(True, None, None, {"parsed_files": len(parsed)})
380
- if assertion == "forbid_call":
381
- call = _config_string(config, "call")
382
- hits = sum(_call_name(node) == call for tree in parsed for node in ast.walk(tree) if isinstance(node, ast.Call))
383
- return _Outcome(hits == 0, None if hits == 0 else "ast_assertion_failed", None, {"call": call, "hits": hits})
384
- return _Outcome(False, "unsupported_ast_assertion", None, {"assertion": assertion})
385
-
386
-
387
- def _argv(config: Mapping[str, object]) -> Optional[list[str]]:
388
- value = config.get("argv")
389
- if not isinstance(value, list) or not value or not all(isinstance(item, str) for item in value):
390
- return None
391
- return list(value)
392
-
393
-
394
- def _command(config: Mapping[str, object], scope: VerificationScope, timeout_cap: Optional[float] = None) -> _Outcome:
395
- argv = _argv(config)
396
- if argv is None:
397
- return _Outcome(False, "invalid_verifier_config", None, {"required": ["argv"]})
398
- cwd = _config_string(config, "cwd") or "."
399
- timeout_value = config.get("timeout", 30)
400
- timeout = float(timeout_value) if isinstance(timeout_value, (int, float)) else 30.0
401
- allowed_value = config.get("allowed_exit_codes", [0])
402
- allowed = tuple(item for item in allowed_value if isinstance(item, int)) if isinstance(allowed_value, list) else (0,)
403
- deadline = None if timeout_cap is None else time.monotonic() + timeout_cap
404
- completed = run_command(argv, str(Path(scope.root) / cwd), timeout, deadline)
405
- outcome = _command_result(completed, argv, timeout, allowed)
406
- return replace(outcome, execution_ms=float(completed.get("execution_ms", 0)),
407
- execution_reused=bool(completed.get("execution_reused")))
408
-
409
-
410
- def _command_result(completed: Mapping[str, object], argv: Sequence[str], timeout: float,
411
- allowed: Sequence[int]) -> _Outcome:
412
- if completed["status"] in ("timeout", "deadline_exceeded"):
413
- return _Outcome(False, "verifier_timeout", None, {"timeout": timeout, "argv": argv})
414
- if completed["status"] == "spawn_error":
415
- return _Outcome(False, "command_unavailable", None, {"argv": argv, "error": completed["stderr"]})
416
- details = {
417
- "argv": argv,
418
- "exit_code": completed["returncode"],
419
- "stdout_sha256": _hash_bytes(str(completed["stdout"]).encode()),
420
- "stderr_sha256": _hash_bytes(str(completed["stderr"]).encode()),
421
- "output": redact_sensitive_text((str(completed["stdout"]) + str(completed["stderr"]))[-4000:]),
422
- }
423
- passed = completed["returncode"] in allowed
424
- return _Outcome(passed, None if passed else "command_failed", None, details)
425
-
426
-
427
- def _manual(config: Mapping[str, object], confirmation: Optional[Mapping[str, object]]) -> _Outcome:
428
- if confirmation is None:
429
- return _Outcome(False, "manual_confirmation_missing", None, {"checklist": config.get("checklist")})
430
- required = ("reason", "confirmed_by", "confirmed_at", "source")
431
- if not all(isinstance(confirmation.get(key), str) and confirmation.get(key) for key in required):
432
- return _Outcome(False, "manual_confirmation_invalid", None, {"required": list(required)})
433
- identity = str(confirmation["confirmed_by"]).lower().split(":", 1)[0]
434
- if identity in ("agent", "assistant", "codex", "claude", "opencode", "costrict"):
435
- return _Outcome(False, "manual_agent_forbidden", None, {"confirmed_by": confirmation["confirmed_by"]})
436
- return _Outcome(True, None, None, {"source": confirmation["source"], "owner": config.get("owner")})
437
-
438
-
439
- def _run(
440
- verifier: SpecVerifier,
441
- metadata: SpecMetadata,
442
- scope: VerificationScope,
443
- confirmation: Optional[Mapping[str, object]],
444
- timeout_cap: Optional[float] = None,
445
- ) -> _Outcome:
446
- if verifier.type == "document":
447
- return _document(verifier.config, scope)
448
- if verifier.type == "regex":
449
- return _regex(verifier.config, metadata, scope)
450
- if verifier.type == "ast":
451
- return _ast(verifier.config, scope)
452
- if verifier.type in ("command", "test"):
453
- return _command(verifier.config, scope, timeout_cap)
454
- if verifier.type == "manual":
455
- return _manual(verifier.config, confirmation)
456
- return _Outcome(False, "verifier_type_unimplemented", None, {"type": verifier.type})
457
-
458
-
459
- def _evidence(
460
- metadata: SpecMetadata,
461
- rule: SpecRule,
462
- verifier: SpecVerifier,
463
- scope: VerificationScope,
464
- confirmation: Optional[Mapping[str, object]],
465
- timeout_cap: Optional[float] = None,
466
- ) -> VerificationEvidence:
467
- started = time.monotonic()
468
- outcome = _run(verifier, metadata, scope, confirmation, timeout_cap)
469
- execution_ms = (time.monotonic() - started) * 1000
470
- if outcome.execution_ms is not None:
471
- execution_ms = outcome.execution_ms
472
- if report_requested():
473
- identity_data = [_cache_key(metadata, rule, verifier, scope, confirmation, verifier.stage),
474
- str(Path(scope.root).resolve()), dict(os.environ), timeout_cap]
475
- identity = hashlib.sha256(json.dumps(identity_data, sort_keys=True, default=str).encode()).hexdigest()
476
- report = {"kind": "spec_check", "check_id": f"{metadata.id}#{rule.ref}",
477
- "reused": int(outcome.execution_reused), "execution_ms": execution_ms}
478
- if not outcome.execution_reused:
479
- report["identity"] = identity
480
- record_report(report)
481
- status = "verified" if outcome.passed else "unverified"
482
- diff_hash = None if verifier.type == "document" else scope.diff_sha256
483
- return VerificationEvidence(
484
- f"{metadata.id}#{rule.ref}",
485
- datetime.now(timezone.utc).isoformat(),
486
- status,
487
- rule.text_sha256,
488
- outcome.artifact_sha256,
489
- diff_hash,
490
- _result_hash(status, outcome.error_code, outcome.details),
491
- outcome.error_code,
492
- outcome.details,
493
- )
494
-
495
-
496
- def _skipped_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope) -> VerificationEvidence:
497
- details = {"skipped": True}
498
- return VerificationEvidence(
499
- f"{metadata.id}#{rule.ref}",
500
- datetime.now(timezone.utc).isoformat(),
501
- "unverified",
502
- rule.text_sha256,
503
- None,
504
- scope.diff_sha256,
505
- _result_hash("unverified", "skipped_in_cheap_gate", details),
506
- "skipped_in_cheap_gate",
507
- details,
508
- )
509
-
510
-
511
- def _deferral_kind(reason: str) -> str:
512
- """Map a task-gate deferral reason to its evidence category.
513
-
514
- Budget and env deferrals are recorded but never treated as a pass: the
515
- required check did not actually run. Only review-stage work may move to the
516
- requirement-level verify-e2e gate.
517
- """
518
- if reason in ("out_of_task_scope", "review_stage"):
519
- return "review_stage"
520
- if reason in ("budget", "over_finish_budget", "verifier_budget_exhausted"):
521
- return "budget"
522
- if reason in ("env", "env_blocked", "environment_unavailable"):
523
- return "env"
524
- return "unknown"
525
-
526
-
527
- def _deferred_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope,
528
- reason: str) -> VerificationEvidence:
529
- """任务级 Done Gate 延后执行(范围外/预算/环境);需求级 verify-e2e 只补 review-stage。"""
530
- kind = _deferral_kind(reason)
531
- error_code = {
532
- "review_stage": "deferred_to_review",
533
- "budget": "verifier_budget_exhausted",
534
- "env": "env_blocked",
535
- }.get(kind, "deferred_to_review")
536
- details = {"reason": reason, "deferral_kind": kind, "deferred_to_review": kind == "review_stage"}
537
- return VerificationEvidence(
538
- f"{metadata.id}#{rule.ref}",
539
- datetime.now(timezone.utc).isoformat(),
540
- "unverified",
541
- rule.text_sha256,
542
- None,
543
- scope.diff_sha256,
544
- _result_hash("unverified", error_code, details),
545
- error_code,
546
- details,
547
- )
548
-
549
-
550
- def _budget_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope, budget: float) -> VerificationEvidence:
551
- details = {"budget": budget, "deferral_kind": "budget"}
552
- return VerificationEvidence(
553
- f"{metadata.id}#{rule.ref}",
554
- datetime.now(timezone.utc).isoformat(),
555
- "unverified",
556
- rule.text_sha256,
557
- None,
558
- scope.diff_sha256,
559
- _result_hash("unverified", "verifier_budget_exhausted", details),
560
- "verifier_budget_exhausted",
561
- details,
562
- )
563
-
564
-
565
- def _missing_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope) -> VerificationEvidence:
566
- details = {"rule": rule.ref}
567
- return VerificationEvidence(
568
- f"{metadata.id}#{rule.ref}", datetime.now(timezone.utc).isoformat(),
569
- "unverified", rule.text_sha256, None, scope.diff_sha256,
570
- _result_hash("unverified", "verifier_missing", details), "verifier_missing", details,
571
- )
572
-
573
-
574
- def _run_all_verifiers(metadata: SpecMetadata, scope: VerificationScope,
575
- confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
576
- skip_command: bool = False,
577
- timeout_budget: Optional[float] = None,
578
- stage: Optional[str] = "code",
579
- deferred: Optional[Mapping[str, str]] = None,
580
- ) -> VerificationResult:
581
- verifier_by_rule = {item.rule: item for item in metadata.verifiers}
582
- confirmation_by_rule = confirmations or {}
583
- deferred_by_rule = deferred or {}
584
- _load_result_cache(scope.root)
585
- evidence_by_ref: dict[str, VerificationEvidence] = {}
586
- parallel: list[tuple[SpecRule, SpecVerifier, Optional[Mapping[str, object]]]] = []
587
- started = time.monotonic()
588
- for rule in metadata.rules:
589
- if rule.enforcement != "required":
590
- continue
591
- verifier = verifier_by_rule.get(rule.ref)
592
- if verifier is not None and stage is not None and verifier.stage != stage:
593
- continue
594
- if rule.ref in deferred_by_rule:
595
- evidence_by_ref[rule.ref] = _deferred_evidence(metadata, rule, scope, deferred_by_rule[rule.ref])
596
- continue
597
- if timeout_budget is not None:
598
- remaining = timeout_budget - (time.monotonic() - started)
599
- if remaining <= 0:
600
- evidence_by_ref[rule.ref] = _budget_evidence(metadata, rule, scope, timeout_budget)
601
- continue
602
- if verifier is None:
603
- evidence_by_ref[rule.ref] = _missing_evidence(metadata, rule, scope)
604
- continue
605
- if skip_command and verifier.type in ("command", "test"):
606
- evidence_by_ref[rule.ref] = _skipped_evidence(metadata, rule, scope)
607
- continue
608
- confirmation = confirmation_by_rule.get(rule.ref)
609
- if timeout_budget is None and verifier.type in ("document", "regex", "ast"):
610
- parallel.append((rule, verifier, confirmation))
611
- continue
612
- cap = None if timeout_budget is None else max(0.1, timeout_budget - (time.monotonic() - started))
613
- evidence_by_ref[rule.ref] = _cached_or_run(metadata, rule, verifier, scope, confirmation, stage) if cap is None else _evidence(metadata, rule, verifier, scope, confirmation, cap)
614
- if parallel:
615
- with ThreadPoolExecutor(max_workers=min(8, len(parallel))) as pool:
616
- futures = {
617
- rule.ref: pool.submit(copy_context().run, _cached_or_run, metadata, rule, verifier, scope, confirmation, stage)
618
- for rule, verifier, confirmation in parallel
619
- }
620
- evidence_by_ref.update({ref: future.result() for ref, future in futures.items()})
621
- result = tuple(evidence_by_ref[rule.ref] for rule in metadata.rules if rule.ref in evidence_by_ref)
622
- _flush_result_cache(scope.root)
623
- return VerificationResult(result, bool(result) and all(item.status == "verified" for item in result))
624
-
625
-
626
- def run_all_verifiers(metadata: SpecMetadata, scope: VerificationScope,
627
- confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
628
- skip_command: bool = False, timeout_budget: Optional[float] = None,
629
- stage: Optional[str] = "code",
630
- deferred: Optional[Mapping[str, str]] = None) -> VerificationResult:
631
- with execution_session():
632
- return _run_all_verifiers(metadata, scope, confirmations, skip_command, timeout_budget, stage, deferred)
633
-
634
-
635
- def evidence_is_fresh(
636
- evidence: VerificationEvidence,
637
- rule_text_sha256: str,
638
- artifact_sha256: Optional[str],
639
- diff_sha256: Optional[str],
640
- ) -> bool:
641
- return (
642
- evidence.status == "verified"
643
- and evidence.rule_text_sha256 == rule_text_sha256
644
- and evidence.artifact_sha256 == artifact_sha256
645
- and evidence.diff_sha256 == diff_sha256
646
- )
647
-
648
- def store_verification_evidence(
649
- metadata: SpecMetadata,
650
- scope: VerificationScope,
651
- result: VerificationResult,
652
- requirement: str = "",
653
- ) -> tuple[dict[str, object], ...]:
654
- """Persist verifier runs in the central sidecar (audit/compaction input)."""
655
- from codeflow.evidence.cf_evidence_store import EvidenceStore, git_commit
656
-
657
- store = EvidenceStore(scope.root, requirement)
658
- commit = git_commit(store.root)
659
- outcomes: list[dict[str, object]] = []
660
- for item in result.evidence:
661
- details = dict(item.details or {})
662
- identity = "|".join((
663
- item.verifier_ref, item.status, item.result_sha256,
664
- item.diff_sha256 or "", item.rule_text_sha256,
665
- ))
666
- argv = details.get("argv")
667
- timeout = details.get("timeout")
668
- run = {
669
- "run_id": hashlib.sha256(identity.encode("utf-8")).hexdigest()[:32],
670
- "check_id": item.verifier_ref,
671
- "command": argv if isinstance(argv, list) and all(isinstance(value, str) for value in argv) else [],
672
- "cwd": ".",
673
- "timeout": timeout if isinstance(timeout, (int, float)) else 0,
674
- "input_hash": item.diff_sha256 or item.result_sha256,
675
- "exit_code": details.get("exit_code") if isinstance(details.get("exit_code"), int) else (0 if item.status == "verified" else 1),
676
- "status": "passed" if item.status == "verified" else "failed",
677
- "executed_at": item.executed_at,
678
- "source": "local",
679
- "commit": commit,
680
- "run_url": "",
681
- "rule_text_sha256": item.rule_text_sha256,
682
- "result_sha256": item.result_sha256,
683
- "error_code": item.error_code,
684
- "details": details,
685
- }
686
- outcomes.append(store.write_run(item.verifier_ref, run))
687
- return tuple(outcomes)