@michaelxwb/code-flow 0.6.5 → 0.8.1

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 (207) hide show
  1. package/README.md +47 -3
  2. package/docs/USAGE.md +1529 -0
  3. package/docs/acceptance-contract.md +217 -0
  4. package/docs/ci/acceptance-evidence.md +77 -0
  5. package/docs/migrations/runtime-0.7.0.md +41 -0
  6. package/docs/migrations/runtime-0.8.0.md +154 -0
  7. package/docs/migrations/runtime-0.9.0.md +40 -0
  8. package/docs/spec-injection-hardening.md +54 -0
  9. package/docs/spec-injection-performance.json +73 -0
  10. package/docs/spec-optimization.md +12 -0
  11. package/package.json +18 -3
  12. package/src/adapters/claude/CLAUDE.md +1 -1
  13. package/src/adapters/claude/commands/cf-init.md +1 -3
  14. package/src/adapters/claude/commands/cf-spec.md +23 -3
  15. package/src/adapters/claude/commands/cf-stats.md +11 -1
  16. package/src/adapters/claude/commands/cf-sync.md +20 -6
  17. package/src/adapters/claude/commands/cf-task/align.md +25 -3
  18. package/src/adapters/claude/commands/cf-task/archive.md +36 -42
  19. package/src/adapters/claude/commands/cf-task/block.md +15 -2
  20. package/src/adapters/claude/commands/cf-task/graph.md +13 -1
  21. package/src/adapters/claude/commands/cf-task/note.md +13 -1
  22. package/src/adapters/claude/commands/cf-task/plan.md +137 -23
  23. package/src/adapters/claude/commands/cf-task/prd.md +13 -2
  24. package/src/adapters/claude/commands/cf-task/start.md +52 -39
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
  27. package/src/adapters/claude/commands/cf-validate.md +30 -5
  28. package/src/adapters/codex/AGENTS.md +1 -1
  29. package/src/adapters/codex/hooks.json +14 -10
  30. package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
  31. package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
  32. package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
  33. package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
  34. package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
  35. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
  36. package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
  37. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
  38. package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
  39. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
  40. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
  41. package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
  42. package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
  43. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
  44. package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
  45. package/src/adapters/commands.json +115 -0
  46. package/src/adapters/costrict/CLAUDE.md +1 -1
  47. package/src/adapters/costrict/commands/cf-init.md +1 -3
  48. package/src/adapters/costrict/commands/cf-spec.md +23 -3
  49. package/src/adapters/costrict/commands/cf-stats.md +11 -1
  50. package/src/adapters/costrict/commands/cf-sync.md +20 -6
  51. package/src/adapters/costrict/commands/cf-task/align.md +25 -3
  52. package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
  53. package/src/adapters/costrict/commands/cf-task/block.md +15 -2
  54. package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
  55. package/src/adapters/costrict/commands/cf-task/note.md +13 -1
  56. package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
  57. package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
  58. package/src/adapters/costrict/commands/cf-task/start.md +52 -39
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
  61. package/src/adapters/costrict/commands/cf-validate.md +30 -5
  62. package/src/adapters/install-plan.json +100 -0
  63. package/src/adapters/opencode/AGENTS.md +1 -1
  64. package/src/adapters/opencode/commands/cf-init.md +7 -7
  65. package/src/adapters/opencode/commands/cf-learn.md +6 -6
  66. package/src/adapters/opencode/commands/cf-spec.md +34 -2
  67. package/src/adapters/opencode/commands/cf-stats.md +17 -7
  68. package/src/adapters/opencode/commands/cf-sync.md +21 -6
  69. package/src/adapters/opencode/commands/cf-task/align.md +25 -8
  70. package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
  71. package/src/adapters/opencode/commands/cf-task/block.md +16 -2
  72. package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
  73. package/src/adapters/opencode/commands/cf-task/note.md +14 -1
  74. package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
  75. package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
  76. package/src/adapters/opencode/commands/cf-task/start.md +53 -45
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
  79. package/src/adapters/opencode/commands/cf-validate.md +35 -9
  80. package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
  81. package/src/adapters/platform-bindings.json +370 -0
  82. package/src/cli.js +369 -316
  83. package/src/core/code-flow/config.yml +4 -1
  84. package/src/core/code-flow/runtime-commands.json +259 -0
  85. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
  86. package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
  87. package/src/core/code-flow/scripts/cf_audit.py +10 -0
  88. package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
  89. package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
  90. package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
  91. package/src/core/code-flow/scripts/cf_feedback.py +5 -51
  92. package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
  93. package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
  94. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
  95. package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
  96. package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
  98. package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
  99. package/src/core/code-flow/scripts/cf_stats.py +5 -501
  100. package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
  101. package/src/core/code-flow/scripts/cf_sync.py +5 -135
  102. package/src/core/code-flow/scripts/cf_task_index.py +5 -143
  103. package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
  105. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
  106. package/src/core/code-flow/scripts/cf_validation.py +6 -77
  107. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  108. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  109. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
  110. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
  111. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
  112. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
  113. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
  114. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
  115. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
  116. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
  117. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
  118. package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
  119. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
  120. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
  121. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
  122. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
  123. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  124. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
  125. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
  126. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
  127. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
  128. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
  129. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
  130. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
  131. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
  132. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
  133. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
  134. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
  135. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
  136. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
  137. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
  138. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
  139. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
  140. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  141. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
  142. package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
  143. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
  144. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
  145. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
  146. package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
  147. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
  148. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
  149. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
  150. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
  151. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
  152. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
  153. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
  154. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
  155. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  156. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
  157. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  158. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
  159. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
  160. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
  161. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
  162. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
  163. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
  164. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  165. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
  166. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
  167. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
  168. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
  169. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
  170. package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
  171. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
  172. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
  173. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
  174. package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
  175. package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
  176. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
  177. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
  178. package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
  179. package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
  180. package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
  181. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
  182. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
  183. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
  184. package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
  185. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  186. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
  187. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
  188. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
  189. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
  190. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
  191. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
  192. package/src/core/code-flow/scripts/runtime-resources.json +29 -0
  193. package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
  194. package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
  195. package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
  196. package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
  197. package/src/core/code-flow/validation.yml +9 -3
  198. package/src/migrate/runtime.js +213 -0
  199. package/src/migrate/spec-workflow.js +16 -1
  200. package/src/runtime.js +219 -0
  201. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  202. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  203. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  204. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  205. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  206. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  207. /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
@@ -0,0 +1,570 @@
1
+ #!/usr/bin/env python3
2
+ """Active-task scope expansion and hash-bound Done verification."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, replace
7
+ import hashlib
8
+ import json
9
+ import math
10
+ import sys
11
+ from pathlib import Path
12
+ import time
13
+ from typing import Mapping, Optional, Sequence
14
+
15
+ from codeflow.cli.cf_spec_context import (
16
+ BindingInput,
17
+ RuleBinding,
18
+ RuleStageStatus,
19
+ SpecBinding,
20
+ SpecContext,
21
+ bind_specs,
22
+ current_owned_paths,
23
+ load_active_task,
24
+ load_context,
25
+ pause_active_task,
26
+ resync_active_hash,
27
+ save_context,
28
+ )
29
+ from codeflow.spec.cf_stage_gate import evidence_deferral_kind, validate_stage
30
+ from codeflow.spec.cf_spec_metadata import SpecMetadata, load_spec_metadata
31
+ from codeflow.spec.cf_spec_resolver import resolve_candidates, SpecCandidate
32
+ from codeflow.spec.cf_spec_context_model import context_sha256
33
+ from codeflow.spec.cf_spec_verify import VerificationEvidence, VerificationScope, run_all_verifiers, store_verification_evidence
34
+ from codeflow.core.cf_core import load_config, phase_timing, resolve_quality_loop
35
+ from codeflow.core import cf_git
36
+ from codeflow.acceptance.cf_acceptance_manifest_core import validate_execution_baseline
37
+ from codeflow.acceptance.cf_acceptance_schema import load_manifest
38
+ from codeflow.core.cf_exec_base import execution_session, record_report
39
+ from codeflow.core.cf_validators import (
40
+ PARSE_ERROR,
41
+ VALID,
42
+ ValidatorConfig,
43
+ is_heavy,
44
+ resolve_validators,
45
+ run_validators,
46
+ trigger_matches,
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ScopeResult:
52
+ decision: str
53
+ files: tuple[str, ...]
54
+ new_specs: tuple[str, ...]
55
+ message: str
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class DoneResult:
60
+ decision: str
61
+ files: tuple[str, ...]
62
+ evidence: tuple[Mapping[str, object], ...]
63
+ message: str = ""
64
+ deferred_review: int = 0
65
+ deferred_requirement: int = 0
66
+ deferred_budget: int = 0
67
+ deferred_heavy: int = 0
68
+ missing_checks: tuple[Mapping[str, object], ...] = ()
69
+ next_action: str = ""
70
+ env_blocked_details: tuple[Mapping[str, object], ...] = ()
71
+
72
+
73
+ def _context_path(task_dir: str) -> str:
74
+ return str(Path(task_dir) / "spec-context.yml")
75
+
76
+
77
+ def _acceptance_baseline_issue(task_dir: str, owner: str) -> str:
78
+ from codeflow.spec.cf_workflow_service import locate_task_file
79
+ path = Path(task_dir) / ".acceptance-manifest.json"
80
+ task = locate_task_file(task_dir, owner)
81
+ try:
82
+ requires = task is not None and "## Acceptance Coverage" in task.read_text(encoding="utf-8")
83
+ if not path.is_file():
84
+ return "acceptance_manifest_missing" if requires else ""
85
+ data = load_manifest(path)
86
+ if requires:
87
+ from codeflow.acceptance.cf_acceptance_manifest_core import validate_manifest
88
+ valid, reason = validate_manifest(str(task), str(path))
89
+ if not valid:
90
+ return reason
91
+ validate_execution_baseline(path, data)
92
+ return ""
93
+ except (OSError, ValueError) as exc:
94
+ return f"acceptance_manifest_invalid: {exc}"
95
+
96
+
97
+ def _required_candidate(candidate: SpecCandidate) -> bool:
98
+ metadata = candidate.metadata
99
+ return metadata.enforcement == "required" and any(
100
+ rule.enforcement == "required" for rule in metadata.rules
101
+ )
102
+
103
+
104
+ def evaluate_scope(root: str, task_dir: str) -> ScopeResult:
105
+ active = load_active_task(root)
106
+ if active.baseline.head:
107
+ try:
108
+ cf_git.run_git(root, ("rev-parse", "--verify", f"{active.baseline.head}^{{commit}}"))
109
+ except cf_git.GitError as exc:
110
+ return ScopeResult("pause", (), (), f"baseline_unavailable: {exc}")
111
+ else:
112
+ return ScopeResult("pause", (), (), "baseline_unavailable: missing task baseline")
113
+ files = current_owned_paths(root, active)
114
+ context = load_context(_context_path(task_dir))
115
+ candidates = resolve_candidates(root, "code", files)
116
+ existing = {binding.spec_id for binding in context.bindings}
117
+ new = tuple(
118
+ candidate
119
+ for candidate in candidates
120
+ if candidate.scope == "path" and candidate.spec_id not in existing
121
+ )
122
+ if not new:
123
+ return ScopeResult("continue", files, (), "scope unchanged")
124
+ selections = tuple(
125
+ BindingInput(candidate, "scope:path", f"Git diff matched {','.join(candidate.matched_paths)}")
126
+ for candidate in new
127
+ )
128
+ save_context(_context_path(task_dir), bind_specs(context, selections))
129
+ # The toolchain expanded the context itself; re-sync the marker so this
130
+ # automatic change does not surface as active_context_drift with no recovery.
131
+ resync_active_hash(root, task_dir, context_sha256(load_context(_context_path(task_dir))))
132
+ required = tuple(candidate.spec_id for candidate in new if _required_candidate(candidate))
133
+ if required:
134
+ pause_active_task(root)
135
+ message = "新增 required Spec 已暂停 TASK;请选择局部 Plan 或回 Align 更新设计"
136
+ return ScopeResult("pause", files, required, message)
137
+ return ScopeResult("continue", files, tuple(candidate.spec_id for candidate in new), "advisory scope expanded")
138
+
139
+
140
+ def _diff_hash(root: str, files: tuple[str, ...]) -> str:
141
+ digest = hashlib.sha256()
142
+ for relative in files:
143
+ path = Path(root) / relative
144
+ digest.update(relative.encode())
145
+ digest.update(path.read_bytes() if path.is_file() else b"<deleted>")
146
+ return digest.hexdigest()
147
+
148
+
149
+ def _evidence_data(evidence: VerificationEvidence) -> Mapping[str, object]:
150
+ return {
151
+ "verifier_ref": evidence.verifier_ref,
152
+ "executed_at": evidence.executed_at,
153
+ "status": evidence.status,
154
+ "rule_text_sha256": evidence.rule_text_sha256,
155
+ "artifact_sha256": evidence.artifact_sha256,
156
+ "diff_sha256": evidence.diff_sha256,
157
+ "result_sha256": evidence.result_sha256,
158
+ "error_code": evidence.error_code,
159
+ "details": evidence.details,
160
+ }
161
+
162
+
163
+ _NON_BLOCKING_EVIDENCE = frozenset(("skipped_in_cheap_gate", "deferred_to_review", "verifier_budget_exhausted"))
164
+
165
+
166
+ def _verified_is_stale(rule: RuleBinding, current: RuleStageStatus, evidence: Mapping[str, object]) -> bool:
167
+ """True when preserved verified evidence no longer matches this scope.
168
+
169
+ A deferral/cheap skip must not un-verify a rule that is still fresh (the
170
+ verifier did not run but the old proof applies to the same diff). Once the
171
+ diff or rule text changes, keeping the verified status would be a stale
172
+ green: fail closed and let the deferral downgrade it.
173
+ """
174
+ latest = current.evidence[-1] if current.evidence else None
175
+ if not isinstance(latest, Mapping):
176
+ return True
177
+ if latest.get("rule_text_sha256") != rule.text_sha256:
178
+ return True
179
+ incoming = evidence.get("diff_sha256")
180
+ existing = latest.get("diff_sha256")
181
+ if isinstance(incoming, str) and incoming and isinstance(existing, str) and existing:
182
+ return incoming != existing
183
+ return False
184
+
185
+
186
+ def _update_rule(rule: RuleBinding, evidence: Mapping[str, object]) -> RuleBinding:
187
+ stage = rule.verifier_stage or "code"
188
+ if stage not in rule.stage_status:
189
+ return rule
190
+ statuses = dict(rule.stage_status)
191
+ current = statuses[stage]
192
+ if current.status in ("not_applicable", "waived"):
193
+ return rule
194
+ # A cheap skip/deferral means "not executed in this pass", not a product
195
+ # failure: never un-verify an already verified rule (and never append
196
+ # deferral noise that hides the real evidence). Stale proof does not count.
197
+ if current.status == "verified" and evidence.get("status") != "verified" and \
198
+ evidence.get("error_code") in _NON_BLOCKING_EVIDENCE and \
199
+ not _verified_is_stale(rule, current, evidence):
200
+ statuses[stage] = current
201
+ return replace(rule, stage_status=statuses)
202
+ status = "verified" if evidence.get("status") == "verified" else "unverified"
203
+ # Cheap-gate skips carry no per-run diff freshness requirement: dedup on
204
+ # (verifier_ref, result_sha256) — the third slot stays None on BOTH sides.
205
+ # Storing a real diff_sha256 in the signature made every Stop append a
206
+ # duplicate skip entry (signature never matched).
207
+ signature = (evidence.get("verifier_ref"), evidence.get("result_sha256"),
208
+ None if evidence.get("error_code") == "skipped_in_cheap_gate" else evidence.get("diff_sha256"))
209
+ if any(
210
+ (item.get("verifier_ref"), item.get("result_sha256"),
211
+ None if item.get("error_code") == "skipped_in_cheap_gate" else item.get("diff_sha256")) == signature
212
+ for item in current.evidence
213
+ ):
214
+ statuses[stage] = replace(current, status=status)
215
+ else:
216
+ statuses[stage] = replace(current, status=status, evidence=(*current.evidence, evidence))
217
+ return replace(rule, stage_status=statuses)
218
+
219
+
220
+ def _apply_evidence(context: SpecContext, evidence: tuple[Mapping[str, object], ...]) -> SpecContext:
221
+ by_ref = {str(item["verifier_ref"]): item for item in evidence}
222
+ bindings: list[SpecBinding] = []
223
+ for binding in context.bindings:
224
+ rules = tuple(
225
+ _update_rule(rule, by_ref[f"{binding.spec_id}#{rule.ref}"])
226
+ if f"{binding.spec_id}#{rule.ref}" in by_ref else rule
227
+ for rule in binding.rules
228
+ )
229
+ bindings.append(replace(binding, rules=rules))
230
+ return replace(context, bindings=tuple(bindings))
231
+
232
+
233
+ def integrate_verification_evidence(
234
+ context: SpecContext, evidence: Sequence[VerificationEvidence],
235
+ ) -> tuple[SpecContext, tuple[Mapping[str, object], ...]]:
236
+ """Apply verifier results with their existing freshness and deduplication rules."""
237
+ records = tuple(_evidence_data(item) for item in evidence)
238
+ return _apply_evidence(context, records), records
239
+
240
+
241
+ def _rule_manual_confirmation(rule: RuleBinding) -> Optional[Mapping[str, object]]:
242
+ status = rule.stage_status.get("code")
243
+ if status is None or status.decision is None or status.decision.kind != "manual_verification":
244
+ return None
245
+ decision = status.decision
246
+ return {
247
+ "reason": decision.reason,
248
+ "confirmed_by": decision.confirmed_by,
249
+ "confirmed_at": decision.confirmed_at,
250
+ "source": decision.source,
251
+ }
252
+
253
+
254
+ def task_spec_scope(root: str, task_dir: str, owner: str, files: tuple[str, ...]) -> set[str]:
255
+ """本任务的验证范围:Spec-Refs ∪ 改动文件经 path_mapping 命中的 spec。
256
+
257
+ 范围外的 code 层 command/test verifier 延后到需求级 verify-e2e 全量补跑。
258
+ """
259
+ scope = {
260
+ candidate.spec_id
261
+ for candidate in resolve_candidates(root, "code", files)
262
+ if candidate.scope == "path"
263
+ }
264
+ from codeflow.spec.cf_workflow_service import locate_task_file
265
+
266
+ task = locate_task_file(task_dir, owner)
267
+ if task is None:
268
+ return scope
269
+ from codeflow.task.cf_task_doc import task_section, task_spec_refs
270
+
271
+ section = task_section(task.read_text(encoding="utf-8"), owner)
272
+ # Older TASK documents may omit Spec-Refs; invalid declared refs must not
273
+ # silently narrow the verification scope to path mappings alone.
274
+ refs = task_spec_refs(section) if "Spec-Refs" in section.fields else ()
275
+ for ref in refs:
276
+ spec_id = ref.split("#", 1)[0].strip()
277
+ if spec_id:
278
+ scope.add(spec_id)
279
+ return scope
280
+
281
+
282
+ def _binding_deferrals(
283
+ binding: SpecBinding,
284
+ metadata: SpecMetadata,
285
+ scope_specs: set[str],
286
+ budget: float,
287
+ run_started: float,
288
+ ) -> dict[str, str]:
289
+ """计算本 binding 内 code 层 command/test verifier 的延后原因。
290
+
291
+ - `review_stage`:不属于本任务(需求级累积绑定),留到 verify-e2e 补跑;
292
+ - `budget`:本次 finish 的真实预算已经耗尽(声明 timeout 本身不是延后理由)。
293
+ document/regex/ast 静态检查始终执行,不参与延后。
294
+ """
295
+ deferred: dict[str, str] = {}
296
+ verifier_by_rule = {item.rule: item for item in metadata.verifiers}
297
+ for rule in binding.rules:
298
+ verifier = verifier_by_rule.get(rule.ref)
299
+ if verifier is None or verifier.type not in ("command", "test"):
300
+ continue
301
+ status = rule.stage_status.get("code")
302
+ if status is None or status.status in ("not_applicable", "waived"):
303
+ continue
304
+ if binding.spec_id not in scope_specs:
305
+ deferred[rule.ref] = "review_stage"
306
+ continue
307
+ # A declared timeout larger than the remaining budget does not prove
308
+ # exhaustion: run it and let the runner cap the subprocess deadline.
309
+ # Only an already exhausted wall-clock budget is a budget deferral.
310
+ if budget > 0 and (time.monotonic() - run_started) >= budget:
311
+ deferred[rule.ref] = "budget"
312
+ return deferred
313
+
314
+
315
+ _NON_EXECUTION_ERRORS = frozenset((
316
+ "skipped_in_cheap_gate",
317
+ "no_matching_files",
318
+ "verifier_missing",
319
+ "command_unavailable",
320
+ "file_read_error",
321
+ "artifact_read_error",
322
+ "regex_unavailable",
323
+ ))
324
+
325
+
326
+ def is_non_execution_error(code: object) -> bool:
327
+ """Distinguish verifier results that did not execute from actual failures."""
328
+ return isinstance(code, str) and code in _NON_EXECUTION_ERRORS
329
+
330
+ _PROVISIONAL_NEXT = (
331
+ "补齐/修复阻塞项后重新运行 task finish;budget/env/未执行不算通过。"
332
+ "review_stage 延后项由需求级 task verify-e2e 补跑。"
333
+ )
334
+
335
+
336
+ def _required_code_gaps(context: SpecContext) -> tuple[tuple[Mapping[str, object], ...], tuple[str, ...]]:
337
+ """Collect required code checks that did not run vs. checks that failed.
338
+
339
+ Provisional is only allowed for non-execution (budget/env/missing). A real
340
+ failing assertion must stay a block so "not executed" never hides a broken
341
+ implementation.
342
+ """
343
+ missing: dict[str, Mapping[str, object]] = {}
344
+ failed: list[str] = []
345
+ for binding in context.bindings:
346
+ for rule in binding.rules:
347
+ if rule.enforcement != "required" or (rule.verifier_stage or "code") != "code":
348
+ continue
349
+ status = rule.stage_status.get("code")
350
+ if status is None or status.status in ("verified", "not_applicable", "waived", "applied"):
351
+ continue
352
+ latest = status.evidence[-1] if status.evidence else None
353
+ latest = latest if isinstance(latest, Mapping) else None
354
+ kind = evidence_deferral_kind(latest)
355
+ if kind in ("review_stage", "heavy"):
356
+ continue
357
+ ref = f"{binding.spec_id}#{rule.ref}"
358
+ error = latest.get("error_code") if latest is not None else None
359
+ if kind in ("budget", "env"):
360
+ missing[ref] = {
361
+ "ref": ref,
362
+ "kind": kind,
363
+ "error_code": error or kind,
364
+ "verifier_ref": rule.verifier_ref,
365
+ }
366
+ elif error is None or is_non_execution_error(error):
367
+ missing[ref] = {
368
+ "ref": ref,
369
+ "kind": "not_executed",
370
+ "error_code": error or status.status,
371
+ "verifier_ref": rule.verifier_ref,
372
+ }
373
+ else:
374
+ failed.append(ref)
375
+ return tuple(missing[key] for key in sorted(missing)), tuple(sorted(set(failed)))
376
+
377
+
378
+ def _provisional_message(missing: tuple[Mapping[str, object], ...]) -> str:
379
+ detail = ", ".join(f"{item['ref']} ({item['kind']})" for item in missing)
380
+ return f"required code checks not executed: {detail}"
381
+
382
+
383
+ def _run_acceptance(root: str, task_dir: str, owner: str, include_e2e: bool,
384
+ deadline: Optional[float], cheap: bool = False
385
+ ) -> tuple[str, str, tuple[Mapping[str, object], ...]]:
386
+ baseline_issue = _acceptance_baseline_issue(task_dir, owner)
387
+ if baseline_issue:
388
+ return baseline_issue, "block", ()
389
+ # manual 场景的人工确认统一在需求级终验(verify-e2e + confirm-manual)完成,
390
+ # 不在每个任务的 Done Gate 中逐任务阻断。
391
+ if cheap:
392
+ # 轻量门禁:只做 manifest 基线检查,不执行 functional 场景;
393
+ # 场景命令由 finish 的全量 Done Gate 执行。
394
+ return "", "pass", ()
395
+ manifest_path = Path(task_dir) / ".acceptance-manifest.json"
396
+ if manifest_path.is_file():
397
+ from codeflow.cli.cf_acceptance_runner import run_manifest
398
+ scenario_result = run_manifest(str(manifest_path), root, write_evidence=True, include_e2e=include_e2e, owner=owner, deadline=deadline)
399
+ if scenario_result["decision"] == "block":
400
+ statuses = [
401
+ str(item.get("status", ""))
402
+ for item in scenario_result.get("results", [])
403
+ if isinstance(item, dict)
404
+ ]
405
+ failed = [value for value in statuses if value in ("failed", "not_configured")]
406
+ if failed:
407
+ reason = scenario_result.get("error") or "acceptance scenario failed"
408
+ return f"acceptance scenario failed: {reason}", "block", ()
409
+ if "env_blocked" in statuses:
410
+ details = tuple(
411
+ item for item in scenario_result.get("env_blocked_details", [])
412
+ if isinstance(item, Mapping)
413
+ )
414
+ return "acceptance env_blocked: " + (scenario_result.get("error") or "environment unavailable"), "env_blocked", details
415
+ if "incomplete" in statuses:
416
+ return "acceptance incomplete: " + (scenario_result.get("error") or "deadline exceeded"), "incomplete", ()
417
+ reason = scenario_result.get("error") or "acceptance scenario failed"
418
+ return f"acceptance scenario failed: {reason}", "block", ()
419
+ return "", "pass", ()
420
+
421
+
422
+ def _run_finish_validation(root: str, files: tuple[str, ...]) -> tuple[str, int]:
423
+ """Light validation.yml once per TASK finish; heavy validators defer by default.
424
+
425
+ Stop-time checks stay cheap; `heavy: true` suites (full tests/builds/e2e) are
426
+ skipped unless `quality_loop.heavy_at_finish: true`, and run again at archive
427
+ `cf_validation` / `/cf-validate`. Returns (issue, deferred_heavy_count).
428
+ """
429
+ quality = resolve_quality_loop(load_config(root))
430
+ if not quality.get("finish_check"):
431
+ return "", 0
432
+ if not files:
433
+ return "", 0
434
+ include_heavy = bool(quality.get("heavy_at_finish"))
435
+ try:
436
+ config, effective = resolve_validators(root)
437
+ if config.state == PARSE_ERROR:
438
+ return f"finish validation blocked: invalid_validation_config: {config.error}", 0
439
+ deferred_heavy = 0 if include_heavy else sum(
440
+ 1
441
+ for validator in effective
442
+ if is_heavy(validator)
443
+ and any(trigger_matches(str(validator.get("trigger", "")), name) for name in files)
444
+ )
445
+ validators = [item for item in effective if include_heavy or not is_heavy(item)]
446
+ if not validators:
447
+ return "", deferred_heavy
448
+ maximum = sum(float(item.get("timeout", 30000)) / 1000 for item in validators)
449
+ if not math.isfinite(maximum) or maximum <= 0:
450
+ raise ValueError("validation budget must be finite and positive")
451
+ with execution_session():
452
+ outcome = run_validators(root, ValidatorConfig(VALID, tuple(validators)), list(files),
453
+ "cf-validate", time.monotonic() + maximum, strict=True, cache=True)
454
+ except (OSError, ValueError) as exc:
455
+ return f"finish validation error: {exc}", 0
456
+ if not outcome.failures and not outcome.truncated:
457
+ return "", deferred_heavy
458
+ if outcome.truncated and not outcome.failures:
459
+ return "finish validation 预算耗尽:部分 validator 未执行", deferred_heavy
460
+ names = (", ".join(str(item.get("name", "validator")) for item in outcome.failures[:3])
461
+ if outcome.failures else "validator")
462
+ suffix = "(预算耗尽,部分未执行)" if outcome.truncated else ""
463
+ return f"finish validation failed{suffix}: {names}", deferred_heavy
464
+
465
+
466
+ def _record_spec_phase(name: str, started: float) -> None:
467
+ record_report({"kind": "spec_phases", "timings": {name: (time.monotonic() - started) * 1000}})
468
+
469
+
470
+ def _run_done_binding(binding: SpecBinding, scope: VerificationScope, task_dir: str, scope_specs: set[str],
471
+ verifier_budget: float, run_started: float, deadline: Optional[float],
472
+ cheap: bool) -> tuple[tuple[VerificationEvidence, ...], int, int, int]:
473
+ phase_started = time.monotonic()
474
+ metadata = load_spec_metadata(str(Path(scope.root) / ".code-flow/specs" / binding.path))
475
+ _record_spec_phase("planning_ms", phase_started)
476
+ deferred_review = sum(1 for rule in binding.rules if rule.verifier_stage == "review")
477
+ deferrals = {} if cheap else _binding_deferrals(binding, metadata, scope_specs, verifier_budget, run_started)
478
+ deferred_requirement = sum(1 for rule in binding.rules
479
+ if deferrals.get(rule.ref) == "review_stage" and rule.enforcement == "required")
480
+ deferred_budget = sum(1 for rule in binding.rules
481
+ if deferrals.get(rule.ref) == "budget" and rule.enforcement == "required")
482
+ confirmations = {rule.ref: value for rule in binding.rules
483
+ if (value := _rule_manual_confirmation(rule)) is not None}
484
+ remaining = None if deadline is None else deadline - time.monotonic()
485
+ result = run_all_verifiers(metadata, scope, confirmations, cheap, remaining, stage="code", deferred=deferrals or None)
486
+ phase_timing(f"done.verify.{binding.spec_id}", phase_started)
487
+ evidence_started = time.monotonic()
488
+ try:
489
+ from codeflow.evidence.cf_evidence_store import requirement_key
490
+ store_verification_evidence(metadata, scope, result, requirement=requirement_key(Path(task_dir)))
491
+ except (OSError, ValueError) as exc:
492
+ sys.stderr.write(f"cf_task_runtime: evidence sidecar write failed: {exc}\n")
493
+ _record_spec_phase("evidence_ms", evidence_started)
494
+ return result.evidence, deferred_review, deferred_requirement, deferred_budget
495
+
496
+
497
+ def _run_done_verifiers(context: SpecContext, scope: VerificationScope, task_dir: str, owner: str,
498
+ deadline: Optional[float], cheap: bool) -> tuple[list[VerificationEvidence], int, int, int]:
499
+ quality = resolve_quality_loop(load_config(scope.root))
500
+ verifier_budget = float(quality.get("finish_verifier_budget", 300.0))
501
+ scope_specs = set() if cheap else task_spec_scope(scope.root, task_dir, owner, scope.files)
502
+ if not cheap and not scope_specs:
503
+ scope_specs = {binding.spec_id for binding in context.bindings}
504
+ run_started = time.monotonic()
505
+ evidence: list[VerificationEvidence] = []
506
+ review = requirement = budget = 0
507
+ for binding in context.bindings:
508
+ records, one_review, one_requirement, one_budget = _run_done_binding(
509
+ binding, scope, task_dir, scope_specs, verifier_budget, run_started, deadline, cheap)
510
+ evidence.extend(records)
511
+ review += one_review
512
+ requirement += one_requirement
513
+ budget += one_budget
514
+ return evidence, review, requirement, budget
515
+
516
+
517
+ def _run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Optional[float] = None, include_e2e: bool = False, task_id: str = "") -> DoneResult:
518
+ active = load_active_task(root)
519
+ owner = task_id or active.task_id
520
+ if owner != active.task_id or (Path(root) / active.task_dir).resolve() != Path(task_dir).resolve():
521
+ return DoneResult("block", (), (), "active_mismatch: task identity does not match marker")
522
+ started = time.monotonic()
523
+ scope_result = evaluate_scope(root, task_dir)
524
+ phase_timing("done.evaluate_scope", started)
525
+ _record_spec_phase("preflight_ms", started)
526
+ if scope_result.decision == "pause":
527
+ return DoneResult("block", scope_result.files, (), scope_result.message)
528
+ deadline = started + budget if budget is not None else None
529
+ issue, acceptance_kind, env_details = _run_acceptance(root, task_dir, owner, include_e2e, deadline, cheap)
530
+ if issue:
531
+ decision = "provisional" if acceptance_kind in ("env_blocked", "incomplete") else "block"
532
+ return DoneResult(decision, scope_result.files, (), issue,
533
+ env_blocked_details=env_details)
534
+ phase_started = time.monotonic()
535
+ context = load_context(_context_path(task_dir))
536
+ diff_hash = _diff_hash(root, scope_result.files)
537
+ phase_timing("done.load_context_and_diff", phase_started)
538
+ _record_spec_phase("planning_ms", phase_started)
539
+ deferred_heavy = 0
540
+ verifier_evidence, deferred_review, deferred_requirement, deferred_budget = _run_done_verifiers(
541
+ context, VerificationScope(root, scope_result.files, diff_hash), task_dir, owner, deadline, cheap)
542
+ evidence_started = time.monotonic()
543
+ updated, all_evidence = integrate_verification_evidence(context, verifier_evidence)
544
+ if updated != context:
545
+ save_context(_context_path(task_dir), updated)
546
+ _record_spec_phase("evidence_ms", evidence_started)
547
+ gate = validate_stage(updated, "code", diff_sha256=diff_hash, allow_cheap_skips=cheap,
548
+ allow_deferred_skips=True)
549
+ if gate.decision == "block" and not cheap:
550
+ missing, failed = _required_code_gaps(updated)
551
+ if missing and not failed:
552
+ budget_gaps = sum(1 for item in missing if item.get("kind") == "budget")
553
+ return DoneResult("provisional", scope_result.files, tuple(all_evidence),
554
+ _provisional_message(missing), deferred_review,
555
+ deferred_requirement, budget_gaps, deferred_heavy,
556
+ missing, _PROVISIONAL_NEXT)
557
+ if gate.decision == "pass" and not cheap:
558
+ validation_issue, deferred_heavy = _run_finish_validation(root, scope_result.files)
559
+ if validation_issue:
560
+ return DoneResult("block", scope_result.files, tuple(all_evidence), validation_issue,
561
+ deferred_review, deferred_requirement, deferred_budget, deferred_heavy)
562
+ phase_timing("done.total", started)
563
+ return DoneResult(gate.decision, scope_result.files, tuple(all_evidence),
564
+ "; ".join(issue.message for issue in gate.errors), deferred_review,
565
+ deferred_requirement, deferred_budget, deferred_heavy)
566
+
567
+
568
+ def run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Optional[float] = None, include_e2e: bool = False, task_id: str = "") -> DoneResult:
569
+ with execution_session():
570
+ return _run_done_gate(root, task_dir, cheap, budget, include_e2e, task_id)
@@ -0,0 +1,73 @@
1
+ """Validation scope and execution shared by task verification and CLI."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from pathlib import Path
6
+ import time
7
+ from typing import Optional, Sequence
8
+
9
+ from codeflow.core import cf_git
10
+ from codeflow.core.cf_exec_base import execution_session
11
+ from codeflow.core.cf_validators import PARSE_ERROR, VALID, ValidatorConfig, is_heavy, resolve_validators, run_validators
12
+ from codeflow.spec.cf_active_task import current_owned_paths, load_active_task
13
+
14
+
15
+ _MANAGED_PREFIXES = (".code-flow/", ".claude/", ".codex/", ".costrict/", ".opencode/", ".agents/")
16
+
17
+
18
+ def _is_managed(path: str) -> bool:
19
+ return path.startswith(_MANAGED_PREFIXES)
20
+
21
+
22
+ def validation_scope(root: str, files: Optional[Sequence[str]] = None) -> tuple[str, ...]:
23
+ base = Path(root).resolve()
24
+ if files is not None:
25
+ selected = []
26
+ for name in files:
27
+ path = (base / name).resolve()
28
+ if base not in path.parents:
29
+ raise ValueError(f"validation path outside project: {name}")
30
+ relative = path.relative_to(base).as_posix()
31
+ if _is_managed(relative):
32
+ continue
33
+ selected.append(relative)
34
+ return tuple(sorted(set(selected)))
35
+ # Tool-managed trees (deployed hooks/commands/plugins) are not project
36
+ # source: validating them makes template validators (eslint/vitest/mypy)
37
+ # fire on every install and blocks verify-e2e for unrelated stacks.
38
+ if (base / ".code-flow/.active-task.json").exists():
39
+ scoped = current_owned_paths(str(base), load_active_task(str(base)))
40
+ else:
41
+ scoped = tuple(sorted(cf_git.changed_files(str(base))))
42
+ return tuple(sorted(item for item in scoped if not _is_managed(item)))
43
+
44
+
45
+ def validate_files(root: str, files: Optional[Sequence[str]] = None, budget: Optional[float] = None,
46
+ include_heavy: bool = True, use_cache: bool = True) -> dict[str, object]:
47
+ started = time.monotonic()
48
+ selected = validation_scope(root, files)
49
+ config, effective = resolve_validators(root)
50
+ if config.state == PARSE_ERROR:
51
+ return {"decision": "block", "files": selected, "reason": "invalid_validation_config",
52
+ "error": config.error}
53
+ validators = list(effective)
54
+ if not include_heavy:
55
+ validators = [item for item in validators if not is_heavy(item)]
56
+ if not selected:
57
+ return {"decision": "pass", "files": [], "reason": "no_changes"}
58
+ if not validators:
59
+ return {"decision": "block", "files": selected, "reason": "no_validators_configured"}
60
+ maximum = budget if budget is not None else sum(float(v.get("timeout", 30000)) / 1000 for v in validators)
61
+ if not math.isfinite(maximum) or maximum <= 0:
62
+ raise ValueError("validation budget must be finite and positive")
63
+ timings = {"planning_ms": (time.monotonic() - started) * 1000,
64
+ "preflight_ms": 0.0, "execution_ms": 0.0, "evidence_ms": 0.0}
65
+ observations: list[dict[str, str]] = []
66
+ with execution_session():
67
+ outcome = run_validators(root, ValidatorConfig(VALID, tuple(validators)), list(selected),
68
+ "cf-validate", time.monotonic() + maximum, strict=True, cache=use_cache,
69
+ timings=timings, observations=observations)
70
+ timings["elapsed_ms"] = (time.monotonic() - started) * 1000
71
+ return {"decision": "block" if outcome.failures or outcome.truncated else "pass", "files": selected,
72
+ "failures": outcome.failures, "incomplete": outcome.truncated, "reused": outcome.reused,
73
+ "timings": timings, "observations": observations}
@@ -0,0 +1,29 @@
1
+ {
2
+ "schema": 1,
3
+ "gitignore": {
4
+ "start": "# >>> code-flow:runtime schema=1",
5
+ "end": "# <<< code-flow:runtime schema=1",
6
+ "entries": [
7
+ ".active-task.json",
8
+ ".active-task.lock",
9
+ ".stop.lock",
10
+ ".catalog-state.json",
11
+ ".session-state.json",
12
+ ".context-state.json",
13
+ ".task-projection-state.json",
14
+ "migrations/",
15
+ "backups/",
16
+ ".debug.log",
17
+ ".session-log.jsonl",
18
+ ".check-state.json",
19
+ "sessions/",
20
+ "specs/_session/",
21
+ "worktrees/",
22
+ ".validation-cache.json",
23
+ ".artifact-hash-cache.json",
24
+ ".runtime-install.json",
25
+ ".runtime-migration.lock/",
26
+ ".verifier-cache.json"
27
+ ]
28
+ }
29
+ }