@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.
- package/README.md +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -42
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +52 -39
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +52 -39
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +53 -45
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
|
@@ -4,24 +4,29 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
import tempfile
|
|
12
|
+
import codeflow.cli.cf_spec_session as cf_spec_session
|
|
11
13
|
from typing import Mapping, Optional, Sequence
|
|
12
14
|
|
|
13
|
-
from cf_core import (
|
|
15
|
+
from codeflow.core.cf_core import (
|
|
14
16
|
assemble_context,
|
|
17
|
+
ConfigError,
|
|
15
18
|
build_effective_mapping,
|
|
16
19
|
build_spec_catalog,
|
|
17
|
-
|
|
20
|
+
estimate_tokens,
|
|
18
21
|
load_config,
|
|
19
|
-
parse_spec_frontmatter,
|
|
20
22
|
resolve_compress,
|
|
21
23
|
)
|
|
22
|
-
from
|
|
23
|
-
from
|
|
24
|
-
from
|
|
24
|
+
from codeflow.core.cf_log import warn
|
|
25
|
+
from codeflow.cli.cf_spec_context import ContextError, SpecContext, load_active_task, load_context
|
|
26
|
+
from codeflow.core.cf_context_cache import load_snapshot
|
|
27
|
+
from codeflow.spec.cf_spec_resolver import resolve_candidate_headers
|
|
28
|
+
from codeflow.spec.cf_spec_context_model import context_sha256
|
|
29
|
+
from codeflow.spec.cf_task_projection import project_task_session
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
_PROJECTION_CACHE_PATH = ".code-flow/.task-projection-state.json"
|
|
@@ -33,6 +38,7 @@ class RouteResult:
|
|
|
33
38
|
text: str
|
|
34
39
|
specs: tuple[str, ...]
|
|
35
40
|
context_sha256: Optional[str] = None
|
|
41
|
+
expanded_specs: tuple[str, ...] = ()
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
class RouterError(ValueError):
|
|
@@ -59,12 +65,21 @@ def _projection_cache_path(root: str) -> Path:
|
|
|
59
65
|
return Path(root) / _PROJECTION_CACHE_PATH
|
|
60
66
|
|
|
61
67
|
|
|
68
|
+
def _context_stat(task_dir: Path) -> list[int]:
|
|
69
|
+
info = (task_dir / "spec-context.yml").stat()
|
|
70
|
+
return [info.st_mtime_ns, info.st_ctime_ns, info.st_size]
|
|
71
|
+
|
|
72
|
+
|
|
62
73
|
def _load_projection_cache(root: str) -> Mapping[str, object]:
|
|
63
74
|
try:
|
|
64
75
|
value = json.loads(_projection_cache_path(root).read_text(encoding="utf-8"))
|
|
65
76
|
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
66
77
|
return {}
|
|
67
|
-
|
|
78
|
+
if not isinstance(value, dict):
|
|
79
|
+
return {}
|
|
80
|
+
checksum = value.pop("checksum", None)
|
|
81
|
+
expected = hashlib.sha256(json.dumps(value, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
82
|
+
return value if checksum == expected else {}
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
def _cached_projection(
|
|
@@ -90,6 +105,12 @@ def _cached_projection(
|
|
|
90
105
|
return None
|
|
91
106
|
if cache.get("task_dir_mtime_ns") != directory_mtime or cache.get("task_mtime_ns") != task_mtime:
|
|
92
107
|
return None
|
|
108
|
+
if cache.get("context_stat") != _context_stat(task_dir):
|
|
109
|
+
return None
|
|
110
|
+
if cache.get("projector_sha256") != hashlib.sha256(Path(cf_spec_session.__file__).read_bytes()).hexdigest():
|
|
111
|
+
return None
|
|
112
|
+
if cache.get("task_sha256") != hashlib.sha256(task_file.read_bytes()).hexdigest():
|
|
113
|
+
return None
|
|
93
114
|
return str(cache["text"]), bool(cache["truncated"])
|
|
94
115
|
|
|
95
116
|
|
|
@@ -107,13 +128,17 @@ def _save_projection_cache(
|
|
|
107
128
|
"version": 1,
|
|
108
129
|
"task_id": task_id,
|
|
109
130
|
"context_sha256": current_hash,
|
|
131
|
+
"context_stat": _context_stat(task_dir),
|
|
132
|
+
"projector_sha256": hashlib.sha256(Path(cf_spec_session.__file__).read_bytes()).hexdigest(),
|
|
110
133
|
"task_file": str(task_file),
|
|
111
134
|
"task_dir_mtime_ns": task_dir.stat().st_mtime_ns,
|
|
112
135
|
"task_mtime_ns": task_file.stat().st_mtime_ns,
|
|
136
|
+
"task_sha256": hashlib.sha256(task_file.read_bytes()).hexdigest(),
|
|
113
137
|
"text": text,
|
|
114
138
|
"truncated": truncated,
|
|
115
139
|
}
|
|
116
140
|
path = _projection_cache_path(root)
|
|
141
|
+
payload["checksum"] = hashlib.sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
117
142
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
143
|
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
119
144
|
temporary.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
|
@@ -126,23 +151,48 @@ def _save_projection_cache(
|
|
|
126
151
|
pass
|
|
127
152
|
|
|
128
153
|
|
|
129
|
-
def
|
|
154
|
+
def _verify_bound_specs(root: str, context: SpecContext) -> None:
|
|
155
|
+
spec_root = Path(root) / ".code-flow/specs"
|
|
156
|
+
for binding in context.bindings:
|
|
157
|
+
target = spec_root / binding.path
|
|
158
|
+
try:
|
|
159
|
+
current = hashlib.sha256(target.read_bytes()).hexdigest()
|
|
160
|
+
except OSError as exc:
|
|
161
|
+
raise RouterError("bound_spec_missing", f"{binding.path}: {exc}; refresh Context") from exc
|
|
162
|
+
if current != binding.hashes.file_sha256:
|
|
163
|
+
raise RouterError("bound_spec_drift", f"{binding.path} changed; refresh Context before coding")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _scope_expansion(root: str, context: SpecContext, paths: Sequence[str]) -> tuple[str, ...]:
|
|
167
|
+
bound = {binding.spec_id for binding in context.bindings}
|
|
168
|
+
candidates = resolve_candidate_headers(
|
|
169
|
+
root, "code", paths, matched_only=True,
|
|
170
|
+
excluded_paths=tuple(binding.path for binding in context.bindings),
|
|
171
|
+
)
|
|
172
|
+
return tuple(item.spec_id for item in candidates
|
|
173
|
+
if item.spec_id not in bound and item.enforcement == "required")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _active_route(root: str, paths: Sequence[str] = (), check_scope: bool = False) -> RouteResult:
|
|
130
177
|
try:
|
|
131
178
|
active = load_active_task(root)
|
|
132
179
|
task_dir = (Path(root) / active.task_dir).resolve()
|
|
133
|
-
context =
|
|
180
|
+
context, current_hash = load_snapshot(root, str(task_dir / "spec-context.yml"), load_context)
|
|
134
181
|
except (ContextError, OSError, ValueError) as exc:
|
|
135
182
|
raise RouterError("invalid_active_task", str(exc)) from exc
|
|
136
|
-
current_hash = context_sha256(context)
|
|
137
183
|
if current_hash != active.context_sha256:
|
|
138
184
|
raise RouterError(
|
|
139
185
|
"active_context_drift",
|
|
140
|
-
"active marker Context hash does not match; run `
|
|
186
|
+
"active marker Context hash does not match; run `code-flow spec refresh --task-dir <dir> --root <root> --json` to re-sync it (refresh updates the marker hash), or `code-flow spec active doctor --resync` with the hash from `code-flow spec status --json`",
|
|
141
187
|
)
|
|
188
|
+
_verify_bound_specs(root, context)
|
|
142
189
|
cached = _cached_projection(root, task_dir, active.task_id, current_hash)
|
|
143
190
|
if cached is None:
|
|
144
191
|
task_file = _task_file(task_dir, active.task_id)
|
|
145
|
-
|
|
192
|
+
try:
|
|
193
|
+
projection = project_task_session(context, str(task_file), active.task_id, context_hash=current_hash)
|
|
194
|
+
except ValueError as exc:
|
|
195
|
+
raise RouterError("invalid_task_projection", str(exc)) from exc
|
|
146
196
|
_save_projection_cache(
|
|
147
197
|
root, task_dir, task_file, active.task_id, current_hash, projection.text, projection.truncated
|
|
148
198
|
)
|
|
@@ -151,30 +201,26 @@ def _active_route(root: str) -> RouteResult:
|
|
|
151
201
|
text, truncated = cached
|
|
152
202
|
if truncated:
|
|
153
203
|
raise RouterError("task_projection_truncated", "split the TASK before coding")
|
|
154
|
-
refs = tuple(binding.spec_id for binding in context.bindings
|
|
155
|
-
|
|
204
|
+
refs = tuple(binding.spec_id for binding in context.bindings
|
|
205
|
+
if any(f"`{binding.spec_id}#{rule.ref}`" in text for rule in binding.rules))
|
|
206
|
+
expanded = _scope_expansion(root, context, paths) if check_scope else ()
|
|
207
|
+
return RouteResult("task", text, refs, current_hash, expanded)
|
|
156
208
|
|
|
157
209
|
|
|
158
210
|
def _read_candidates(root: str, paths: Sequence[str], compress: bool = True) -> RouteResult:
|
|
211
|
+
from codeflow.spec.cf_spec_budget import SpecBudgetError, path_specs
|
|
159
212
|
try:
|
|
160
|
-
|
|
161
|
-
except
|
|
162
|
-
raise RouterError(
|
|
163
|
-
specs = []
|
|
164
|
-
for candidate in candidates:
|
|
165
|
-
if candidate.scope == "unmatched":
|
|
166
|
-
continue
|
|
167
|
-
target = Path(root) / ".code-flow/specs" / candidate.path
|
|
168
|
-
try:
|
|
169
|
-
raw = target.read_text(encoding="utf-8")
|
|
170
|
-
except OSError as exc:
|
|
171
|
-
raise RouterError("spec_read_failed", f"{candidate.path}: {exc}") from exc
|
|
172
|
-
unused_frontmatter, content = parse_spec_frontmatter(raw)
|
|
173
|
-
del unused_frontmatter
|
|
174
|
-
final_content = compress_content(content.strip()) if compress else content.strip()
|
|
175
|
-
specs.append({"path": candidate.path, "content": final_content, "tier": 1})
|
|
213
|
+
specs = path_specs(root, paths, compress)
|
|
214
|
+
except SpecBudgetError as exc:
|
|
215
|
+
raise RouterError(exc.code, str(exc)) from exc
|
|
176
216
|
text = assemble_context(specs, "## Active Specs (path-mapped)") if specs else ""
|
|
177
|
-
|
|
217
|
+
budget = load_config(root).get("budget", {})
|
|
218
|
+
if not isinstance(budget, dict):
|
|
219
|
+
raise RouterError("invalid_budget", "budget must be a mapping")
|
|
220
|
+
maximum = min(int(budget.get("total", 2500)), int(budget.get("l1_max", 1700)))
|
|
221
|
+
if maximum <= 0 or estimate_tokens(text) > maximum:
|
|
222
|
+
raise RouterError("spec_budget_exceeded", f"Matched Specs exceed {maximum} estimated tokens; create a focused TASK Context or split the editing scope. Required constraints were not truncated.")
|
|
223
|
+
return RouteResult("path", text, tuple(item["path"] for item in specs))
|
|
178
224
|
|
|
179
225
|
|
|
180
226
|
def _catalog_path(root: str) -> Path:
|
|
@@ -191,6 +237,8 @@ def _load_catalog_state(root: str) -> dict[str, object]:
|
|
|
191
237
|
raise RouterError("invalid_catalog_state", str(exc)) from exc
|
|
192
238
|
if not isinstance(value, dict) or value.get("version") != 1:
|
|
193
239
|
raise RouterError("invalid_catalog_state", "Catalog state schema must be version 1")
|
|
240
|
+
if any(type(value.get(key)) is not int or value[key] < 0 for key in ("prompt_count", "last_emitted")):
|
|
241
|
+
raise RouterError("invalid_catalog_state", "Catalog counters must be nonnegative integers")
|
|
194
242
|
return value
|
|
195
243
|
|
|
196
244
|
|
|
@@ -208,7 +256,7 @@ def _save_catalog_state(root: str, state: Mapping[str, object]) -> None:
|
|
|
208
256
|
except OSError as exc:
|
|
209
257
|
if temporary and Path(temporary).exists():
|
|
210
258
|
Path(temporary).unlink()
|
|
211
|
-
|
|
259
|
+
warn(f"Spec Catalog dedup state could not be saved: {exc}")
|
|
212
260
|
|
|
213
261
|
|
|
214
262
|
def _catalog_route(root: str, session_id: str, config: Mapping[str, object]) -> RouteResult:
|
|
@@ -221,20 +269,32 @@ def _catalog_route(root: str, session_id: str, config: Mapping[str, object]) ->
|
|
|
221
269
|
window = int(catalog_config.get("dedup_window", 5))
|
|
222
270
|
due = window <= 0 or last == 0 or count - last >= window
|
|
223
271
|
next_state = {"version": 1, "session_id": session_id, "prompt_count": count, "last_emitted": count if due else last}
|
|
224
|
-
_save_catalog_state(root, next_state)
|
|
225
272
|
if not due:
|
|
273
|
+
_save_catalog_state(root, next_state)
|
|
226
274
|
return RouteResult("catalog", "", ())
|
|
227
275
|
budget = config.get("budget") if isinstance(config.get("budget"), dict) else {}
|
|
228
276
|
maximum = int(budget.get("catalog_max", 200))
|
|
229
277
|
mapping = build_effective_mapping(root, config.get("path_mapping") or {})
|
|
230
|
-
|
|
278
|
+
text = build_spec_catalog(root, mapping, maximum)
|
|
279
|
+
from codeflow.spec.cf_injection import defer_commit
|
|
280
|
+
defer_commit(lambda: _save_catalog_state(root, next_state))
|
|
281
|
+
return RouteResult("catalog", text, ())
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def route_prompt(root: str, paths: Sequence[str], session_id: str, *, check_scope: bool = False) -> RouteResult:
|
|
285
|
+
try:
|
|
286
|
+
return _route_prompt(root, paths, session_id, check_scope)
|
|
287
|
+
except RouterError:
|
|
288
|
+
raise
|
|
289
|
+
except (ConfigError, ContextError, OSError, UnicodeError, ValueError, TypeError) as exc:
|
|
290
|
+
raise RouterError(getattr(exc, "code", "invalid_routing_input"), str(exc)) from exc
|
|
231
291
|
|
|
232
292
|
|
|
233
|
-
def
|
|
293
|
+
def _route_prompt(root: str, paths: Sequence[str], session_id: str, check_scope: bool) -> RouteResult:
|
|
234
294
|
"""Choose exactly one route; an invalid active marker never falls back."""
|
|
235
295
|
marker = Path(root) / ".code-flow/.active-task.json"
|
|
236
296
|
if marker.exists():
|
|
237
|
-
return _active_route(root)
|
|
297
|
+
return _active_route(root, paths, check_scope)
|
|
238
298
|
config = load_config(root)
|
|
239
299
|
if not config:
|
|
240
300
|
return RouteResult("none", "", ())
|
|
@@ -9,16 +9,18 @@ from datetime import datetime, timezone
|
|
|
9
9
|
import fnmatch
|
|
10
10
|
import hashlib
|
|
11
11
|
import json
|
|
12
|
+
import os
|
|
12
13
|
from pathlib import Path
|
|
13
|
-
import subprocess
|
|
14
14
|
from threading import Lock
|
|
15
15
|
import time
|
|
16
16
|
from concurrent.futures import ThreadPoolExecutor
|
|
17
|
+
from contextvars import copy_context
|
|
17
18
|
from typing import Mapping, Optional, Sequence
|
|
18
19
|
|
|
19
|
-
from cf_checks import run_regex_verifier
|
|
20
|
-
from cf_spec_metadata import SpecMetadata, SpecRule, SpecVerifier
|
|
21
|
-
from cf_exec_base import execution_session, run_command
|
|
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
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
_RESULT_CACHE: dict[str, dict[str, object]] = {}
|
|
@@ -87,18 +89,6 @@ def _matches_scope(path: str, patterns: Sequence[str]) -> bool:
|
|
|
87
89
|
return False
|
|
88
90
|
|
|
89
91
|
|
|
90
|
-
def _git_tracked_files(root: str) -> tuple[str, ...]:
|
|
91
|
-
try:
|
|
92
|
-
outcome = subprocess.run(
|
|
93
|
-
("git", "ls-files"), cwd=root, text=True, encoding="utf-8", capture_output=True, check=False
|
|
94
|
-
)
|
|
95
|
-
except OSError:
|
|
96
|
-
return ()
|
|
97
|
-
if outcome.returncode != 0:
|
|
98
|
-
return ()
|
|
99
|
-
return tuple(_normalize_scope_path(line) for line in outcome.stdout.splitlines() if line.strip())
|
|
100
|
-
|
|
101
|
-
|
|
102
92
|
def scoped_files(verifier: SpecVerifier, scope: VerificationScope) -> tuple[str, ...]:
|
|
103
93
|
"""作用域命中的文件集合:git tracked ∪ 任务持有文件,按 verifier.files 过滤。
|
|
104
94
|
|
|
@@ -107,7 +97,7 @@ def scoped_files(verifier: SpecVerifier, scope: VerificationScope) -> tuple[str,
|
|
|
107
97
|
if not verifier.files:
|
|
108
98
|
return ()
|
|
109
99
|
candidates = {_normalize_scope_path(item) for item in scope.files if item.strip()}
|
|
110
|
-
candidates.update(
|
|
100
|
+
candidates.update(cf_git.tracked_files(scope.root))
|
|
111
101
|
return tuple(sorted(path for path in candidates if _matches_scope(path, verifier.files)))
|
|
112
102
|
|
|
113
103
|
|
|
@@ -220,6 +210,10 @@ def _cached_or_run(
|
|
|
220
210
|
confirmation: Optional[Mapping[str, object]],
|
|
221
211
|
stage: str = "code",
|
|
222
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)
|
|
223
217
|
cacheable = verifier.type in _PURE_VERIFIER_TYPES
|
|
224
218
|
scoped_key: Optional[str] = None
|
|
225
219
|
if cacheable:
|
|
@@ -233,6 +227,8 @@ def _cached_or_run(
|
|
|
233
227
|
if cacheable:
|
|
234
228
|
cached = _cached_evidence(key)
|
|
235
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})
|
|
236
232
|
if scoped_key is None:
|
|
237
233
|
return cached
|
|
238
234
|
details = dict(cached.details or {})
|
|
@@ -256,6 +252,8 @@ class _Outcome:
|
|
|
256
252
|
error_code: Optional[str]
|
|
257
253
|
artifact_sha256: Optional[str]
|
|
258
254
|
details: Mapping[str, object]
|
|
255
|
+
execution_ms: Optional[float] = None
|
|
256
|
+
execution_reused: bool = False
|
|
259
257
|
|
|
260
258
|
|
|
261
259
|
def _hash_bytes(data: bytes) -> str:
|
|
@@ -298,10 +296,31 @@ def _document(config: Mapping[str, object], scope: VerificationScope) -> _Outcom
|
|
|
298
296
|
except (OSError, UnicodeError) as exc:
|
|
299
297
|
return _Outcome(False, "artifact_read_error", None, {"artifact": relative, "error": str(exc)})
|
|
300
298
|
expected = _config_string(config, "artifact_sha256")
|
|
301
|
-
|
|
299
|
+
from codeflow.spec.cf_rule_readiness import referenced_item, unresolved_text
|
|
300
|
+
selected = referenced_item(content, section, item)
|
|
301
|
+
present = bool(selected)
|
|
302
302
|
hash_matches = expected is None or expected == artifact_hash
|
|
303
|
-
|
|
304
|
-
|
|
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())
|
|
305
324
|
|
|
306
325
|
|
|
307
326
|
def _regex(config: Mapping[str, object], metadata: SpecMetadata, scope: VerificationScope) -> _Outcome:
|
|
@@ -316,11 +335,16 @@ def _regex(config: Mapping[str, object], metadata: SpecMetadata, scope: Verifica
|
|
|
316
335
|
files = check.get("files") if isinstance(check.get("files"), str) else "*"
|
|
317
336
|
if pattern is None:
|
|
318
337
|
return _Outcome(False, "invalid_verifier_config", None, {"required": ["pattern"]})
|
|
319
|
-
selected =
|
|
320
|
-
|
|
321
|
-
for relative in scope.
|
|
322
|
-
|
|
323
|
-
|
|
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})
|
|
324
348
|
contents, error = _read_scope_files(VerificationScope(scope.root, selected, scope.diff_sha256))
|
|
325
349
|
if error is not None:
|
|
326
350
|
return error
|
|
@@ -378,6 +402,13 @@ def _command(config: Mapping[str, object], scope: VerificationScope, timeout_cap
|
|
|
378
402
|
allowed = tuple(item for item in allowed_value if isinstance(item, int)) if isinstance(allowed_value, list) else (0,)
|
|
379
403
|
deadline = None if timeout_cap is None else time.monotonic() + timeout_cap
|
|
380
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:
|
|
381
412
|
if completed["status"] in ("timeout", "deadline_exceeded"):
|
|
382
413
|
return _Outcome(False, "verifier_timeout", None, {"timeout": timeout, "argv": argv})
|
|
383
414
|
if completed["status"] == "spawn_error":
|
|
@@ -387,7 +418,7 @@ def _command(config: Mapping[str, object], scope: VerificationScope, timeout_cap
|
|
|
387
418
|
"exit_code": completed["returncode"],
|
|
388
419
|
"stdout_sha256": _hash_bytes(str(completed["stdout"]).encode()),
|
|
389
420
|
"stderr_sha256": _hash_bytes(str(completed["stderr"]).encode()),
|
|
390
|
-
"output": (str(completed["stdout"]) + str(completed["stderr"]))[-4000:],
|
|
421
|
+
"output": redact_sensitive_text((str(completed["stdout"]) + str(completed["stderr"]))[-4000:]),
|
|
391
422
|
}
|
|
392
423
|
passed = completed["returncode"] in allowed
|
|
393
424
|
return _Outcome(passed, None if passed else "command_failed", None, details)
|
|
@@ -433,7 +464,20 @@ def _evidence(
|
|
|
433
464
|
confirmation: Optional[Mapping[str, object]],
|
|
434
465
|
timeout_cap: Optional[float] = None,
|
|
435
466
|
) -> VerificationEvidence:
|
|
467
|
+
started = time.monotonic()
|
|
436
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)
|
|
437
481
|
status = "verified" if outcome.passed else "unverified"
|
|
438
482
|
diff_hash = None if verifier.type == "document" else scope.diff_sha256
|
|
439
483
|
return VerificationEvidence(
|
|
@@ -464,8 +508,47 @@ def _skipped_evidence(metadata: SpecMetadata, rule: SpecRule, scope: Verificatio
|
|
|
464
508
|
)
|
|
465
509
|
|
|
466
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
|
+
|
|
467
550
|
def _budget_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope, budget: float) -> VerificationEvidence:
|
|
468
|
-
details = {"budget": budget}
|
|
551
|
+
details = {"budget": budget, "deferral_kind": "budget"}
|
|
469
552
|
return VerificationEvidence(
|
|
470
553
|
f"{metadata.id}#{rule.ref}",
|
|
471
554
|
datetime.now(timezone.utc).isoformat(),
|
|
@@ -488,16 +571,16 @@ def _missing_evidence(metadata: SpecMetadata, rule: SpecRule, scope: Verificatio
|
|
|
488
571
|
)
|
|
489
572
|
|
|
490
573
|
|
|
491
|
-
def _run_all_verifiers(
|
|
492
|
-
metadata: SpecMetadata,
|
|
493
|
-
scope: VerificationScope,
|
|
574
|
+
def _run_all_verifiers(metadata: SpecMetadata, scope: VerificationScope,
|
|
494
575
|
confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
|
|
495
576
|
skip_command: bool = False,
|
|
496
577
|
timeout_budget: Optional[float] = None,
|
|
497
|
-
stage: str = "code",
|
|
578
|
+
stage: Optional[str] = "code",
|
|
579
|
+
deferred: Optional[Mapping[str, str]] = None,
|
|
498
580
|
) -> VerificationResult:
|
|
499
581
|
verifier_by_rule = {item.rule: item for item in metadata.verifiers}
|
|
500
582
|
confirmation_by_rule = confirmations or {}
|
|
583
|
+
deferred_by_rule = deferred or {}
|
|
501
584
|
_load_result_cache(scope.root)
|
|
502
585
|
evidence_by_ref: dict[str, VerificationEvidence] = {}
|
|
503
586
|
parallel: list[tuple[SpecRule, SpecVerifier, Optional[Mapping[str, object]]]] = []
|
|
@@ -506,7 +589,10 @@ def _run_all_verifiers(
|
|
|
506
589
|
if rule.enforcement != "required":
|
|
507
590
|
continue
|
|
508
591
|
verifier = verifier_by_rule.get(rule.ref)
|
|
509
|
-
if verifier is not None and verifier.stage != stage:
|
|
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])
|
|
510
596
|
continue
|
|
511
597
|
if timeout_budget is not None:
|
|
512
598
|
remaining = timeout_budget - (time.monotonic() - started)
|
|
@@ -528,7 +614,7 @@ def _run_all_verifiers(
|
|
|
528
614
|
if parallel:
|
|
529
615
|
with ThreadPoolExecutor(max_workers=min(8, len(parallel))) as pool:
|
|
530
616
|
futures = {
|
|
531
|
-
rule.ref: pool.submit(_cached_or_run, metadata, rule, verifier, scope, confirmation, stage)
|
|
617
|
+
rule.ref: pool.submit(copy_context().run, _cached_or_run, metadata, rule, verifier, scope, confirmation, stage)
|
|
532
618
|
for rule, verifier, confirmation in parallel
|
|
533
619
|
}
|
|
534
620
|
evidence_by_ref.update({ref: future.result() for ref, future in futures.items()})
|
|
@@ -540,9 +626,10 @@ def _run_all_verifiers(
|
|
|
540
626
|
def run_all_verifiers(metadata: SpecMetadata, scope: VerificationScope,
|
|
541
627
|
confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
|
|
542
628
|
skip_command: bool = False, timeout_budget: Optional[float] = None,
|
|
543
|
-
stage: str = "code"
|
|
629
|
+
stage: Optional[str] = "code",
|
|
630
|
+
deferred: Optional[Mapping[str, str]] = None) -> VerificationResult:
|
|
544
631
|
with execution_session():
|
|
545
|
-
return _run_all_verifiers(metadata, scope, confirmations, skip_command, timeout_budget, stage)
|
|
632
|
+
return _run_all_verifiers(metadata, scope, confirmations, skip_command, timeout_budget, stage, deferred)
|
|
546
633
|
|
|
547
634
|
|
|
548
635
|
def evidence_is_fresh(
|
|
@@ -557,3 +644,44 @@ def evidence_is_fresh(
|
|
|
557
644
|
and evidence.artifact_sha256 == artifact_sha256
|
|
558
645
|
and evidence.diff_sha256 == diff_sha256
|
|
559
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)
|