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