@michaelxwb/code-flow 0.6.5 → 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.
- package/README.md +75 -110
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +268 -0
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +21 -9
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -264
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -67
- package/src/adapters/claude/commands/cf-stats.md +0 -86
- package/src/adapters/claude/commands/cf-sync.md +0 -25
- package/src/adapters/claude/commands/cf-task/align.md +0 -309
- package/src/adapters/claude/commands/cf-task/archive.md +0 -161
- package/src/adapters/claude/commands/cf-task/block.md +0 -36
- package/src/adapters/claude/commands/cf-task/graph.md +0 -80
- package/src/adapters/claude/commands/cf-task/note.md +0 -236
- package/src/adapters/claude/commands/cf-task/plan.md +0 -363
- package/src/adapters/claude/commands/cf-task/prd.md +0 -162
- package/src/adapters/claude/commands/cf-task/start.md +0 -304
- package/src/adapters/claude/commands/cf-task/status.md +0 -59
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/claude/commands/cf-validate.md +0 -75
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -50
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -264
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -67
- package/src/adapters/costrict/commands/cf-stats.md +0 -86
- package/src/adapters/costrict/commands/cf-sync.md +0 -25
- package/src/adapters/costrict/commands/cf-task/align.md +0 -309
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
- package/src/adapters/costrict/commands/cf-task/block.md +0 -36
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
- package/src/adapters/costrict/commands/cf-task/note.md +0 -236
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
- package/src/adapters/costrict/commands/cf-task/start.md +0 -304
- package/src/adapters/costrict/commands/cf-task/status.md +0 -59
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/costrict/commands/cf-validate.md +0 -75
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -59
- package/src/adapters/opencode/commands/cf-stats.md +0 -90
- package/src/adapters/opencode/commands/cf-sync.md +0 -29
- package/src/adapters/opencode/commands/cf-task/align.md +0 -314
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
- package/src/adapters/opencode/commands/cf-task/block.md +0 -40
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
- package/src/adapters/opencode/commands/cf-task/note.md +0 -240
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
- package/src/adapters/opencode/commands/cf-task/start.md +0 -309
- package/src/adapters/opencode/commands/cf-task/status.md +0 -63
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/opencode/commands/cf-validate.md +0 -79
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/cli.js +0 -1023
- package/src/core/code-flow/config.yml +0 -158
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_checks.py +0 -336
- package/src/core/code-flow/scripts/cf_core.py +0 -787
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_feedback.py +0 -56
- package/src/core/code-flow/scripts/cf_log.py +0 -154
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
- package/src/core/code-flow/scripts/cf_scan.py +0 -295
- package/src/core/code-flow/scripts/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
- package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
- package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
- package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
- package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
- package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
- package/src/core/code-flow/scripts/cf_stats.py +0 -506
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
- package/src/core/code-flow/scripts/cf_sync.py +0 -140
- package/src/core/code-flow/scripts/cf_task_index.py +0 -148
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
- 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_task_workflow.py +0 -205
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
- package/src/core/code-flow/scripts/cf_validation.py +0 -81
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -53
- package/src/migrate/spec-workflow.js +0 -238
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Project one TASK's bound rules into a bounded session document."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
import json
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
import re
|
|
11
|
-
import sys
|
|
12
|
-
from typing import IO, Mapping, Optional, Sequence
|
|
13
|
-
|
|
14
|
-
from cf_spec_context import RuleBinding, SpecContext, context_sha256, load_context
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass(frozen=True)
|
|
18
|
-
class SessionProjection:
|
|
19
|
-
text: str
|
|
20
|
-
included_rules: int
|
|
21
|
-
total_rules: int
|
|
22
|
-
truncated: bool
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# context_sha256 is defined next to the identity projection in cf_spec_context
|
|
26
|
-
# and re-exported here for callers that import the session module.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _task_section(text: str, task_id: str) -> str:
|
|
30
|
-
match = re.search(rf"(?m)^## {re.escape(task_id)}:[^\n]*$", text)
|
|
31
|
-
if match is None:
|
|
32
|
-
raise ValueError(f"task_not_found: {task_id}")
|
|
33
|
-
following = re.search(r"(?m)^## TASK-\d+:", text[match.end():])
|
|
34
|
-
end = match.end() + following.start() if following else len(text)
|
|
35
|
-
return text[match.start():end]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _refs(section: str) -> tuple[str, ...]:
|
|
39
|
-
# [ \t]* 而非 \s*:空 Spec-Refs 不得跨行吞掉下一字段(Acceptance-Refs 等)
|
|
40
|
-
match = re.search(r"(?m)^- \*\*Spec-Refs\*\*:[ \t]*(.*)$", section)
|
|
41
|
-
if match is None:
|
|
42
|
-
raise ValueError("spec_refs_missing")
|
|
43
|
-
return tuple(item.strip() for item in match.group(1).split(",") if item.strip())
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _rule_index(context: SpecContext) -> Mapping[str, RuleBinding]:
|
|
47
|
-
return {
|
|
48
|
-
f"{binding.spec_id}#{rule.ref}": rule
|
|
49
|
-
for binding in context.bindings
|
|
50
|
-
for rule in binding.rules
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _acceptance_contract(section: str) -> str:
|
|
55
|
-
match = re.search(r"(?ms)^### Acceptance Contract\s*$\n(.*?)(?=^### |^## |\Z)", section)
|
|
56
|
-
if match is None:
|
|
57
|
-
raise ValueError("acceptance_contract_missing")
|
|
58
|
-
return match.group(1).strip()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _rule_line(ref: str, rule: RuleBinding, compact: bool = False) -> str:
|
|
62
|
-
hashes = f"rule_sha256={rule.text_sha256} verifier={rule.verifier_ref}"
|
|
63
|
-
if compact:
|
|
64
|
-
return f"- `{ref}` {hashes}\n"
|
|
65
|
-
artifacts = sorted({item.artifact for status in rule.stage_status.values() for item in status.refs})
|
|
66
|
-
return f"- `{ref}`: {rule.summary}\n - {hashes}; artifacts={','.join(artifacts) or 'none'}\n"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def project_task_session(
|
|
70
|
-
context: SpecContext, task_file: str, task_id: str, max_chars: int = 12000
|
|
71
|
-
) -> SessionProjection:
|
|
72
|
-
section = _task_section(Path(task_file).read_text(encoding="utf-8"), task_id)
|
|
73
|
-
refs = _refs(section)
|
|
74
|
-
index = _rule_index(context)
|
|
75
|
-
missing = tuple(ref for ref in refs if ref not in index)
|
|
76
|
-
if missing:
|
|
77
|
-
raise ValueError(f"unknown_spec_refs: {', '.join(missing)}")
|
|
78
|
-
acceptance = _acceptance_contract(section)
|
|
79
|
-
header = f"# {task_id} Spec Context\n\n- Context-SHA256: `{context_sha256(context)}`\n\n## Required Rules\n"
|
|
80
|
-
footer = f"\n## Acceptance Contract\n{acceptance}\n"
|
|
81
|
-
lines: list[str] = []
|
|
82
|
-
for ref in refs:
|
|
83
|
-
candidate = _rule_line(ref, index[ref])
|
|
84
|
-
if len(header + "".join(lines) + candidate + footer) > max_chars:
|
|
85
|
-
break
|
|
86
|
-
lines.append(candidate)
|
|
87
|
-
truncated = len(lines) < len(refs) or len(header + "".join(lines) + footer) > max_chars
|
|
88
|
-
if truncated:
|
|
89
|
-
footer = "\n> 投影超过预算;完整 Context 保留在需求目录,请拆分 TASK。\n" + footer
|
|
90
|
-
text = (header + "".join(lines) + footer)[:max_chars]
|
|
91
|
-
return SessionProjection(text, len(lines), len(refs), truncated)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _parser() -> argparse.ArgumentParser:
|
|
95
|
-
parser = argparse.ArgumentParser(prog="cf_spec_session.py")
|
|
96
|
-
parser.add_argument("--task-dir", required=True)
|
|
97
|
-
parser.add_argument("--task-file", required=True)
|
|
98
|
-
parser.add_argument("--task", required=True)
|
|
99
|
-
parser.add_argument("--output", required=True)
|
|
100
|
-
parser.add_argument("--budget", type=int, default=12000)
|
|
101
|
-
parser.add_argument("--json", action="store_true")
|
|
102
|
-
return parser
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
106
|
-
try:
|
|
107
|
-
args = _parser().parse_args(argv)
|
|
108
|
-
context = load_context(str(Path(args.task_dir) / "spec-context.yml"))
|
|
109
|
-
result = project_task_session(context, args.task_file, args.task, args.budget)
|
|
110
|
-
Path(args.output).write_text(result.text, encoding="utf-8")
|
|
111
|
-
stdout.write(json.dumps(result.__dict__, ensure_ascii=False))
|
|
112
|
-
return 0
|
|
113
|
-
except Exception as exc:
|
|
114
|
-
stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
|
|
115
|
-
return 3
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if __name__ == "__main__":
|
|
119
|
-
raise SystemExit(main())
|
|
@@ -1,559 +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
|
-
from pathlib import Path
|
|
13
|
-
import subprocess
|
|
14
|
-
from threading import Lock
|
|
15
|
-
import time
|
|
16
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
17
|
-
from typing import Mapping, Optional, Sequence
|
|
18
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
_RESULT_CACHE: dict[str, dict[str, object]] = {}
|
|
25
|
-
_RESULT_CACHE_ROOT: Optional[str] = None
|
|
26
|
-
_RESULT_CACHE_DIRTY = False
|
|
27
|
-
_RESULT_CACHE_LOCK = Lock()
|
|
28
|
-
_MAX_RESULT_CACHE_ENTRIES = 2048
|
|
29
|
-
_PURE_VERIFIER_TYPES = frozenset(("document", "regex", "ast"))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass(frozen=True)
|
|
33
|
-
class VerificationScope:
|
|
34
|
-
root: str
|
|
35
|
-
files: tuple[str, ...]
|
|
36
|
-
diff_sha256: Optional[str]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@dataclass(frozen=True)
|
|
40
|
-
class VerificationEvidence:
|
|
41
|
-
verifier_ref: str
|
|
42
|
-
executed_at: str
|
|
43
|
-
status: str
|
|
44
|
-
rule_text_sha256: str
|
|
45
|
-
artifact_sha256: Optional[str]
|
|
46
|
-
diff_sha256: Optional[str]
|
|
47
|
-
result_sha256: str
|
|
48
|
-
error_code: Optional[str]
|
|
49
|
-
details: Mapping[str, object]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass(frozen=True)
|
|
53
|
-
class VerificationResult:
|
|
54
|
-
evidence: tuple[VerificationEvidence, ...]
|
|
55
|
-
passed: bool
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _cache_file(root: str) -> Path:
|
|
59
|
-
return Path(root) / ".code-flow" / ".verifier-cache.json"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _cache_key(metadata: SpecMetadata, rule: SpecRule, verifier: SpecVerifier, scope: VerificationScope, confirmation: Optional[Mapping[str, object]], stage: str = "code") -> str:
|
|
63
|
-
payload = {
|
|
64
|
-
"spec": metadata.hashes.file_sha256,
|
|
65
|
-
"rule": rule.text_sha256,
|
|
66
|
-
"verifier": {"type": verifier.type, "config": verifier.config},
|
|
67
|
-
"files": scope.files,
|
|
68
|
-
"diff": scope.diff_sha256,
|
|
69
|
-
"stage": stage,
|
|
70
|
-
"confirmation": confirmation or {},
|
|
71
|
-
"artifact_content": _artifact_fingerprint(verifier, scope),
|
|
72
|
-
}
|
|
73
|
-
return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _normalize_scope_path(value: str) -> str:
|
|
77
|
-
return value.replace("\\", "/").strip()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _matches_scope(path: str, patterns: Sequence[str]) -> bool:
|
|
81
|
-
for pattern in patterns:
|
|
82
|
-
if fnmatch.fnmatchcase(path, pattern):
|
|
83
|
-
return True
|
|
84
|
-
bare = pattern.rstrip("/")
|
|
85
|
-
if not any(char in pattern for char in "*?[") and path.startswith(bare + "/"):
|
|
86
|
-
return True
|
|
87
|
-
return False
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
def scoped_files(verifier: SpecVerifier, scope: VerificationScope) -> tuple[str, ...]:
|
|
103
|
-
"""作用域命中的文件集合:git tracked ∪ 任务持有文件,按 verifier.files 过滤。
|
|
104
|
-
|
|
105
|
-
决议仓库级集合(而非仅当前任务 diff)才能让未触及作用域的其他任务命中缓存。
|
|
106
|
-
"""
|
|
107
|
-
if not verifier.files:
|
|
108
|
-
return ()
|
|
109
|
-
candidates = {_normalize_scope_path(item) for item in scope.files if item.strip()}
|
|
110
|
-
candidates.update(_git_tracked_files(scope.root))
|
|
111
|
-
return tuple(sorted(path for path in candidates if _matches_scope(path, verifier.files)))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _scope_fingerprint(root: str, files: Sequence[str]) -> str:
|
|
115
|
-
digest = hashlib.sha256()
|
|
116
|
-
for relative in files:
|
|
117
|
-
path = Path(root) / relative
|
|
118
|
-
digest.update(relative.encode("utf-8"))
|
|
119
|
-
try:
|
|
120
|
-
digest.update(path.read_bytes() if path.is_file() else b"<deleted>")
|
|
121
|
-
except OSError:
|
|
122
|
-
digest.update(b"<unreadable>")
|
|
123
|
-
return digest.hexdigest()
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _scoped_cache_key(
|
|
127
|
-
metadata: SpecMetadata,
|
|
128
|
-
rule: SpecRule,
|
|
129
|
-
verifier: SpecVerifier,
|
|
130
|
-
scope: VerificationScope,
|
|
131
|
-
confirmation: Optional[Mapping[str, object]],
|
|
132
|
-
stage: str,
|
|
133
|
-
) -> Optional[str]:
|
|
134
|
-
"""command/test 仅在声明 files 作用域时缓存;未声明保持全量执行。"""
|
|
135
|
-
if verifier.type not in ("command", "test") or not verifier.files:
|
|
136
|
-
return None
|
|
137
|
-
files = scoped_files(verifier, scope)
|
|
138
|
-
payload = {
|
|
139
|
-
"spec": metadata.hashes.file_sha256,
|
|
140
|
-
"rule": rule.text_sha256,
|
|
141
|
-
"verifier": {"type": verifier.type, "config": verifier.config, "files": list(verifier.files)},
|
|
142
|
-
"stage": stage,
|
|
143
|
-
"scope_files": files,
|
|
144
|
-
"scope_sha256": _scope_fingerprint(scope.root, files),
|
|
145
|
-
"confirmation": confirmation or {},
|
|
146
|
-
}
|
|
147
|
-
return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _artifact_fingerprint(verifier: SpecVerifier, scope: VerificationScope) -> Optional[str]:
|
|
151
|
-
if verifier.type != "document":
|
|
152
|
-
return None
|
|
153
|
-
relative = verifier.config.get("artifact")
|
|
154
|
-
if not isinstance(relative, str):
|
|
155
|
-
return "invalid"
|
|
156
|
-
try:
|
|
157
|
-
return hashlib.sha256((Path(scope.root) / relative).read_bytes()).hexdigest()
|
|
158
|
-
except OSError as exc:
|
|
159
|
-
return f"unreadable:{exc}"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _load_result_cache(root: str) -> None:
|
|
163
|
-
global _RESULT_CACHE_ROOT, _RESULT_CACHE, _RESULT_CACHE_DIRTY
|
|
164
|
-
if _RESULT_CACHE_ROOT == root:
|
|
165
|
-
return
|
|
166
|
-
_RESULT_CACHE_ROOT = root
|
|
167
|
-
_RESULT_CACHE = {}
|
|
168
|
-
_RESULT_CACHE_DIRTY = False
|
|
169
|
-
path = _cache_file(root)
|
|
170
|
-
try:
|
|
171
|
-
loaded = json.loads(path.read_text(encoding="utf-8"))
|
|
172
|
-
if isinstance(loaded, dict):
|
|
173
|
-
_RESULT_CACHE.update({str(key): value for key, value in loaded.items() if isinstance(value, dict)})
|
|
174
|
-
except (OSError, ValueError):
|
|
175
|
-
pass
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _cached_evidence(key: str) -> Optional[VerificationEvidence]:
|
|
179
|
-
item = _RESULT_CACHE.get(key)
|
|
180
|
-
if not isinstance(item, dict) or item.get("status") != "verified":
|
|
181
|
-
return None
|
|
182
|
-
try:
|
|
183
|
-
return VerificationEvidence(**item)
|
|
184
|
-
except (TypeError, ValueError):
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def _save_result_cache(key: str, evidence: VerificationEvidence) -> None:
|
|
189
|
-
global _RESULT_CACHE_DIRTY
|
|
190
|
-
with _RESULT_CACHE_LOCK:
|
|
191
|
-
_RESULT_CACHE[key] = evidence.__dict__
|
|
192
|
-
_RESULT_CACHE_DIRTY = True
|
|
193
|
-
while len(_RESULT_CACHE) > _MAX_RESULT_CACHE_ENTRIES:
|
|
194
|
-
_RESULT_CACHE.pop(next(iter(_RESULT_CACHE)))
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def _flush_result_cache(root: str) -> None:
|
|
198
|
-
global _RESULT_CACHE_DIRTY
|
|
199
|
-
with _RESULT_CACHE_LOCK:
|
|
200
|
-
if not _RESULT_CACHE_DIRTY:
|
|
201
|
-
return
|
|
202
|
-
path = _cache_file(root)
|
|
203
|
-
try:
|
|
204
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
-
temporary = path.with_suffix(".json.tmp")
|
|
206
|
-
temporary.write_text(
|
|
207
|
-
json.dumps(_RESULT_CACHE, ensure_ascii=False, sort_keys=True, default=str), encoding="utf-8"
|
|
208
|
-
)
|
|
209
|
-
temporary.replace(path)
|
|
210
|
-
_RESULT_CACHE_DIRTY = False
|
|
211
|
-
except OSError:
|
|
212
|
-
pass
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _cached_or_run(
|
|
216
|
-
metadata: SpecMetadata,
|
|
217
|
-
rule: SpecRule,
|
|
218
|
-
verifier: SpecVerifier,
|
|
219
|
-
scope: VerificationScope,
|
|
220
|
-
confirmation: Optional[Mapping[str, object]],
|
|
221
|
-
stage: str = "code",
|
|
222
|
-
) -> VerificationEvidence:
|
|
223
|
-
cacheable = verifier.type in _PURE_VERIFIER_TYPES
|
|
224
|
-
scoped_key: Optional[str] = None
|
|
225
|
-
if cacheable:
|
|
226
|
-
key = _cache_key(metadata, rule, verifier, scope, confirmation, stage)
|
|
227
|
-
else:
|
|
228
|
-
scoped_key = _scoped_cache_key(metadata, rule, verifier, scope, confirmation, stage)
|
|
229
|
-
if scoped_key is None:
|
|
230
|
-
key = ""
|
|
231
|
-
else:
|
|
232
|
-
cacheable, key = True, scoped_key
|
|
233
|
-
if cacheable:
|
|
234
|
-
cached = _cached_evidence(key)
|
|
235
|
-
if cached is not None:
|
|
236
|
-
if scoped_key is None:
|
|
237
|
-
return cached
|
|
238
|
-
details = dict(cached.details or {})
|
|
239
|
-
details["cache_reused"] = True
|
|
240
|
-
details["scope_files"] = list(scoped_files(verifier, scope))
|
|
241
|
-
return replace(
|
|
242
|
-
cached,
|
|
243
|
-
executed_at=datetime.now(timezone.utc).isoformat(),
|
|
244
|
-
diff_sha256=scope.diff_sha256,
|
|
245
|
-
details=details,
|
|
246
|
-
)
|
|
247
|
-
current = _evidence(metadata, rule, verifier, scope, confirmation)
|
|
248
|
-
if cacheable and current.status == "verified":
|
|
249
|
-
_save_result_cache(key, current)
|
|
250
|
-
return current
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
@dataclass(frozen=True)
|
|
254
|
-
class _Outcome:
|
|
255
|
-
passed: bool
|
|
256
|
-
error_code: Optional[str]
|
|
257
|
-
artifact_sha256: Optional[str]
|
|
258
|
-
details: Mapping[str, object]
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def _hash_bytes(data: bytes) -> str:
|
|
262
|
-
return hashlib.sha256(data).hexdigest()
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def _result_hash(status: str, error: Optional[str], details: Mapping[str, object]) -> str:
|
|
266
|
-
payload = {"status": status, "error_code": error, "details": details}
|
|
267
|
-
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
268
|
-
return _hash_bytes(encoded)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def _config_string(config: Mapping[str, object], key: str) -> Optional[str]:
|
|
272
|
-
value = config.get(key)
|
|
273
|
-
return value if isinstance(value, str) and value else None
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def _read_scope_files(scope: VerificationScope) -> tuple[dict[str, str], Optional[_Outcome]]:
|
|
277
|
-
contents: dict[str, str] = {}
|
|
278
|
-
for relative in scope.files:
|
|
279
|
-
path = Path(scope.root) / relative
|
|
280
|
-
try:
|
|
281
|
-
contents[relative] = path.read_text(encoding="utf-8")
|
|
282
|
-
except (OSError, UnicodeError) as exc:
|
|
283
|
-
outcome = _Outcome(False, "file_read_error", None, {"file": relative, "error": str(exc)})
|
|
284
|
-
return {}, outcome
|
|
285
|
-
return contents, None
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _document(config: Mapping[str, object], scope: VerificationScope) -> _Outcome:
|
|
289
|
-
relative = _config_string(config, "artifact")
|
|
290
|
-
section = _config_string(config, "section_id")
|
|
291
|
-
item = _config_string(config, "item_id")
|
|
292
|
-
if not relative or not section or not item:
|
|
293
|
-
return _Outcome(False, "invalid_verifier_config", None, {"required": ["artifact", "section_id", "item_id"]})
|
|
294
|
-
path = Path(scope.root) / relative
|
|
295
|
-
try:
|
|
296
|
-
content = path.read_text(encoding="utf-8")
|
|
297
|
-
artifact_hash = _hash_bytes(path.read_bytes())
|
|
298
|
-
except (OSError, UnicodeError) as exc:
|
|
299
|
-
return _Outcome(False, "artifact_read_error", None, {"artifact": relative, "error": str(exc)})
|
|
300
|
-
expected = _config_string(config, "artifact_sha256")
|
|
301
|
-
present = section in content and item in content
|
|
302
|
-
hash_matches = expected is None or expected == artifact_hash
|
|
303
|
-
details = {"artifact": relative, "section_present": section in content, "item_present": item in content, "hash_matches": hash_matches}
|
|
304
|
-
return _Outcome(present and hash_matches, None if present and hash_matches else "document_mismatch", artifact_hash, details)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def _regex(config: Mapping[str, object], metadata: SpecMetadata, scope: VerificationScope) -> _Outcome:
|
|
308
|
-
pattern = _config_string(config, "pattern")
|
|
309
|
-
files = _config_string(config, "files") or "*"
|
|
310
|
-
check_id = _config_string(config, "check_id")
|
|
311
|
-
if check_id:
|
|
312
|
-
check = next((item for item in metadata.checks if item.get("id") == check_id), None)
|
|
313
|
-
if check is None:
|
|
314
|
-
return _Outcome(False, "check_not_found", None, {"check_id": check_id})
|
|
315
|
-
pattern = check.get("pattern") if isinstance(check.get("pattern"), str) else None
|
|
316
|
-
files = check.get("files") if isinstance(check.get("files"), str) else "*"
|
|
317
|
-
if pattern is None:
|
|
318
|
-
return _Outcome(False, "invalid_verifier_config", None, {"required": ["pattern"]})
|
|
319
|
-
selected = tuple(
|
|
320
|
-
relative
|
|
321
|
-
for relative in scope.files
|
|
322
|
-
if fnmatch.fnmatch(relative, files) and (Path(scope.root) / relative).is_file()
|
|
323
|
-
)
|
|
324
|
-
contents, error = _read_scope_files(VerificationScope(scope.root, selected, scope.diff_sha256))
|
|
325
|
-
if error is not None:
|
|
326
|
-
return error
|
|
327
|
-
violations, skipped = run_regex_verifier(pattern, files, contents)
|
|
328
|
-
if skipped:
|
|
329
|
-
return _Outcome(False, "regex_unavailable", None, {"skipped": skipped})
|
|
330
|
-
details = {"checked_files": len(contents), "violations": violations}
|
|
331
|
-
return _Outcome(not violations, None if not violations else "regex_violation", None, details)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def _call_name(node: ast.Call) -> str:
|
|
335
|
-
if isinstance(node.func, ast.Name):
|
|
336
|
-
return node.func.id
|
|
337
|
-
if isinstance(node.func, ast.Attribute):
|
|
338
|
-
return node.func.attr
|
|
339
|
-
return ""
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def _ast(config: Mapping[str, object], scope: VerificationScope) -> _Outcome:
|
|
343
|
-
if _config_string(config, "language") != "python":
|
|
344
|
-
return _Outcome(False, "unsupported_ast_language", None, {"language": config.get("language")})
|
|
345
|
-
assertion = _config_string(config, "assertion") or "parse"
|
|
346
|
-
contents, error = _read_scope_files(scope)
|
|
347
|
-
if error is not None:
|
|
348
|
-
return error
|
|
349
|
-
parsed: list[ast.AST] = []
|
|
350
|
-
try:
|
|
351
|
-
parsed = [ast.parse(content, filename=relative) for relative, content in contents.items()]
|
|
352
|
-
except SyntaxError as exc:
|
|
353
|
-
return _Outcome(False, "ast_parse_error", None, {"error": str(exc)})
|
|
354
|
-
if assertion == "parse":
|
|
355
|
-
return _Outcome(True, None, None, {"parsed_files": len(parsed)})
|
|
356
|
-
if assertion == "forbid_call":
|
|
357
|
-
call = _config_string(config, "call")
|
|
358
|
-
hits = sum(_call_name(node) == call for tree in parsed for node in ast.walk(tree) if isinstance(node, ast.Call))
|
|
359
|
-
return _Outcome(hits == 0, None if hits == 0 else "ast_assertion_failed", None, {"call": call, "hits": hits})
|
|
360
|
-
return _Outcome(False, "unsupported_ast_assertion", None, {"assertion": assertion})
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def _argv(config: Mapping[str, object]) -> Optional[list[str]]:
|
|
364
|
-
value = config.get("argv")
|
|
365
|
-
if not isinstance(value, list) or not value or not all(isinstance(item, str) for item in value):
|
|
366
|
-
return None
|
|
367
|
-
return list(value)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def _command(config: Mapping[str, object], scope: VerificationScope, timeout_cap: Optional[float] = None) -> _Outcome:
|
|
371
|
-
argv = _argv(config)
|
|
372
|
-
if argv is None:
|
|
373
|
-
return _Outcome(False, "invalid_verifier_config", None, {"required": ["argv"]})
|
|
374
|
-
cwd = _config_string(config, "cwd") or "."
|
|
375
|
-
timeout_value = config.get("timeout", 30)
|
|
376
|
-
timeout = float(timeout_value) if isinstance(timeout_value, (int, float)) else 30.0
|
|
377
|
-
allowed_value = config.get("allowed_exit_codes", [0])
|
|
378
|
-
allowed = tuple(item for item in allowed_value if isinstance(item, int)) if isinstance(allowed_value, list) else (0,)
|
|
379
|
-
deadline = None if timeout_cap is None else time.monotonic() + timeout_cap
|
|
380
|
-
completed = run_command(argv, str(Path(scope.root) / cwd), timeout, deadline)
|
|
381
|
-
if completed["status"] in ("timeout", "deadline_exceeded"):
|
|
382
|
-
return _Outcome(False, "verifier_timeout", None, {"timeout": timeout, "argv": argv})
|
|
383
|
-
if completed["status"] == "spawn_error":
|
|
384
|
-
return _Outcome(False, "command_unavailable", None, {"argv": argv, "error": completed["stderr"]})
|
|
385
|
-
details = {
|
|
386
|
-
"argv": argv,
|
|
387
|
-
"exit_code": completed["returncode"],
|
|
388
|
-
"stdout_sha256": _hash_bytes(str(completed["stdout"]).encode()),
|
|
389
|
-
"stderr_sha256": _hash_bytes(str(completed["stderr"]).encode()),
|
|
390
|
-
"output": (str(completed["stdout"]) + str(completed["stderr"]))[-4000:],
|
|
391
|
-
}
|
|
392
|
-
passed = completed["returncode"] in allowed
|
|
393
|
-
return _Outcome(passed, None if passed else "command_failed", None, details)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def _manual(config: Mapping[str, object], confirmation: Optional[Mapping[str, object]]) -> _Outcome:
|
|
397
|
-
if confirmation is None:
|
|
398
|
-
return _Outcome(False, "manual_confirmation_missing", None, {"checklist": config.get("checklist")})
|
|
399
|
-
required = ("reason", "confirmed_by", "confirmed_at", "source")
|
|
400
|
-
if not all(isinstance(confirmation.get(key), str) and confirmation.get(key) for key in required):
|
|
401
|
-
return _Outcome(False, "manual_confirmation_invalid", None, {"required": list(required)})
|
|
402
|
-
identity = str(confirmation["confirmed_by"]).lower().split(":", 1)[0]
|
|
403
|
-
if identity in ("agent", "assistant", "codex", "claude", "opencode", "costrict"):
|
|
404
|
-
return _Outcome(False, "manual_agent_forbidden", None, {"confirmed_by": confirmation["confirmed_by"]})
|
|
405
|
-
return _Outcome(True, None, None, {"source": confirmation["source"], "owner": config.get("owner")})
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
def _run(
|
|
409
|
-
verifier: SpecVerifier,
|
|
410
|
-
metadata: SpecMetadata,
|
|
411
|
-
scope: VerificationScope,
|
|
412
|
-
confirmation: Optional[Mapping[str, object]],
|
|
413
|
-
timeout_cap: Optional[float] = None,
|
|
414
|
-
) -> _Outcome:
|
|
415
|
-
if verifier.type == "document":
|
|
416
|
-
return _document(verifier.config, scope)
|
|
417
|
-
if verifier.type == "regex":
|
|
418
|
-
return _regex(verifier.config, metadata, scope)
|
|
419
|
-
if verifier.type == "ast":
|
|
420
|
-
return _ast(verifier.config, scope)
|
|
421
|
-
if verifier.type in ("command", "test"):
|
|
422
|
-
return _command(verifier.config, scope, timeout_cap)
|
|
423
|
-
if verifier.type == "manual":
|
|
424
|
-
return _manual(verifier.config, confirmation)
|
|
425
|
-
return _Outcome(False, "verifier_type_unimplemented", None, {"type": verifier.type})
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
def _evidence(
|
|
429
|
-
metadata: SpecMetadata,
|
|
430
|
-
rule: SpecRule,
|
|
431
|
-
verifier: SpecVerifier,
|
|
432
|
-
scope: VerificationScope,
|
|
433
|
-
confirmation: Optional[Mapping[str, object]],
|
|
434
|
-
timeout_cap: Optional[float] = None,
|
|
435
|
-
) -> VerificationEvidence:
|
|
436
|
-
outcome = _run(verifier, metadata, scope, confirmation, timeout_cap)
|
|
437
|
-
status = "verified" if outcome.passed else "unverified"
|
|
438
|
-
diff_hash = None if verifier.type == "document" else scope.diff_sha256
|
|
439
|
-
return VerificationEvidence(
|
|
440
|
-
f"{metadata.id}#{rule.ref}",
|
|
441
|
-
datetime.now(timezone.utc).isoformat(),
|
|
442
|
-
status,
|
|
443
|
-
rule.text_sha256,
|
|
444
|
-
outcome.artifact_sha256,
|
|
445
|
-
diff_hash,
|
|
446
|
-
_result_hash(status, outcome.error_code, outcome.details),
|
|
447
|
-
outcome.error_code,
|
|
448
|
-
outcome.details,
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
def _skipped_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope) -> VerificationEvidence:
|
|
453
|
-
details = {"skipped": True}
|
|
454
|
-
return VerificationEvidence(
|
|
455
|
-
f"{metadata.id}#{rule.ref}",
|
|
456
|
-
datetime.now(timezone.utc).isoformat(),
|
|
457
|
-
"unverified",
|
|
458
|
-
rule.text_sha256,
|
|
459
|
-
None,
|
|
460
|
-
scope.diff_sha256,
|
|
461
|
-
_result_hash("unverified", "skipped_in_cheap_gate", details),
|
|
462
|
-
"skipped_in_cheap_gate",
|
|
463
|
-
details,
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
def _budget_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope, budget: float) -> VerificationEvidence:
|
|
468
|
-
details = {"budget": budget}
|
|
469
|
-
return VerificationEvidence(
|
|
470
|
-
f"{metadata.id}#{rule.ref}",
|
|
471
|
-
datetime.now(timezone.utc).isoformat(),
|
|
472
|
-
"unverified",
|
|
473
|
-
rule.text_sha256,
|
|
474
|
-
None,
|
|
475
|
-
scope.diff_sha256,
|
|
476
|
-
_result_hash("unverified", "verifier_budget_exhausted", details),
|
|
477
|
-
"verifier_budget_exhausted",
|
|
478
|
-
details,
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
def _missing_evidence(metadata: SpecMetadata, rule: SpecRule, scope: VerificationScope) -> VerificationEvidence:
|
|
483
|
-
details = {"rule": rule.ref}
|
|
484
|
-
return VerificationEvidence(
|
|
485
|
-
f"{metadata.id}#{rule.ref}", datetime.now(timezone.utc).isoformat(),
|
|
486
|
-
"unverified", rule.text_sha256, None, scope.diff_sha256,
|
|
487
|
-
_result_hash("unverified", "verifier_missing", details), "verifier_missing", details,
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def _run_all_verifiers(
|
|
492
|
-
metadata: SpecMetadata,
|
|
493
|
-
scope: VerificationScope,
|
|
494
|
-
confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
|
|
495
|
-
skip_command: bool = False,
|
|
496
|
-
timeout_budget: Optional[float] = None,
|
|
497
|
-
stage: str = "code",
|
|
498
|
-
) -> VerificationResult:
|
|
499
|
-
verifier_by_rule = {item.rule: item for item in metadata.verifiers}
|
|
500
|
-
confirmation_by_rule = confirmations or {}
|
|
501
|
-
_load_result_cache(scope.root)
|
|
502
|
-
evidence_by_ref: dict[str, VerificationEvidence] = {}
|
|
503
|
-
parallel: list[tuple[SpecRule, SpecVerifier, Optional[Mapping[str, object]]]] = []
|
|
504
|
-
started = time.monotonic()
|
|
505
|
-
for rule in metadata.rules:
|
|
506
|
-
if rule.enforcement != "required":
|
|
507
|
-
continue
|
|
508
|
-
verifier = verifier_by_rule.get(rule.ref)
|
|
509
|
-
if verifier is not None and verifier.stage != stage:
|
|
510
|
-
continue
|
|
511
|
-
if timeout_budget is not None:
|
|
512
|
-
remaining = timeout_budget - (time.monotonic() - started)
|
|
513
|
-
if remaining <= 0:
|
|
514
|
-
evidence_by_ref[rule.ref] = _budget_evidence(metadata, rule, scope, timeout_budget)
|
|
515
|
-
continue
|
|
516
|
-
if verifier is None:
|
|
517
|
-
evidence_by_ref[rule.ref] = _missing_evidence(metadata, rule, scope)
|
|
518
|
-
continue
|
|
519
|
-
if skip_command and verifier.type in ("command", "test"):
|
|
520
|
-
evidence_by_ref[rule.ref] = _skipped_evidence(metadata, rule, scope)
|
|
521
|
-
continue
|
|
522
|
-
confirmation = confirmation_by_rule.get(rule.ref)
|
|
523
|
-
if timeout_budget is None and verifier.type in ("document", "regex", "ast"):
|
|
524
|
-
parallel.append((rule, verifier, confirmation))
|
|
525
|
-
continue
|
|
526
|
-
cap = None if timeout_budget is None else max(0.1, timeout_budget - (time.monotonic() - started))
|
|
527
|
-
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)
|
|
528
|
-
if parallel:
|
|
529
|
-
with ThreadPoolExecutor(max_workers=min(8, len(parallel))) as pool:
|
|
530
|
-
futures = {
|
|
531
|
-
rule.ref: pool.submit(_cached_or_run, metadata, rule, verifier, scope, confirmation, stage)
|
|
532
|
-
for rule, verifier, confirmation in parallel
|
|
533
|
-
}
|
|
534
|
-
evidence_by_ref.update({ref: future.result() for ref, future in futures.items()})
|
|
535
|
-
result = tuple(evidence_by_ref[rule.ref] for rule in metadata.rules if rule.ref in evidence_by_ref)
|
|
536
|
-
_flush_result_cache(scope.root)
|
|
537
|
-
return VerificationResult(result, bool(result) and all(item.status == "verified" for item in result))
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def run_all_verifiers(metadata: SpecMetadata, scope: VerificationScope,
|
|
541
|
-
confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
|
|
542
|
-
skip_command: bool = False, timeout_budget: Optional[float] = None,
|
|
543
|
-
stage: str = "code") -> VerificationResult:
|
|
544
|
-
with execution_session():
|
|
545
|
-
return _run_all_verifiers(metadata, scope, confirmations, skip_command, timeout_budget, stage)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
def evidence_is_fresh(
|
|
549
|
-
evidence: VerificationEvidence,
|
|
550
|
-
rule_text_sha256: str,
|
|
551
|
-
artifact_sha256: Optional[str],
|
|
552
|
-
diff_sha256: Optional[str],
|
|
553
|
-
) -> bool:
|
|
554
|
-
return (
|
|
555
|
-
evidence.status == "verified"
|
|
556
|
-
and evidence.rule_text_sha256 == rule_text_sha256
|
|
557
|
-
and evidence.artifact_sha256 == artifact_sha256
|
|
558
|
-
and evidence.diff_sha256 == diff_sha256
|
|
559
|
-
)
|