@michaelxwb/code-flow 0.6.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -40
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +67 -35
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -40
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +67 -35
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +73 -34
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -40
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +67 -35
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -40
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +68 -41
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1714
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -380
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +37 -3
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +252 -26
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -299
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only invariant audit for design acceptance data, manifest and evidence.
|
|
3
|
+
|
|
4
|
+
The report is derived, never hand-maintained:
|
|
5
|
+
|
|
6
|
+
- scenario / rule / risk counts come from the design §2.5.1, §2.5.2 and §5 tables;
|
|
7
|
+
- owners come from the manifest ``owner`` field, never from prose;
|
|
8
|
+
- verified evidence is resolved from the central evidence sidecar first, with
|
|
9
|
+
the manifest run record as a compatibility fallback for pre-sidecar runs;
|
|
10
|
+
- findings always carry ``scenario_id`` and ``rule_ref`` so they can be fixed
|
|
11
|
+
without guessing which row or invariant failed.
|
|
12
|
+
|
|
13
|
+
The audit only reads files. It never rewrites the manifest, task document,
|
|
14
|
+
design document or evidence sidecar.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from typing import IO, Mapping, Optional, Sequence
|
|
26
|
+
|
|
27
|
+
from codeflow.acceptance.cf_acceptance_schema import (check_outcome, load_manifest, recorded_check_hash,
|
|
28
|
+
scenario_check_ids, verified_evidence)
|
|
29
|
+
from codeflow.acceptance.cf_acceptance_risk import risk_counts
|
|
30
|
+
from codeflow.evidence.cf_evidence_store import EvidenceStore
|
|
31
|
+
from codeflow.core.cf_markdown import column_index, is_separator_row, table_cells
|
|
32
|
+
from codeflow.acceptance.cf_traceability import build_traceability, build_traceability_many, build_traceability_from_scenarios
|
|
33
|
+
|
|
34
|
+
_SCENARIO_RE = re.compile(r"\b[SEB]-\d+\b")
|
|
35
|
+
_RULE_RE = re.compile(r"^RULE-\d+$")
|
|
36
|
+
_TASK_RE = re.compile(r"^TASK-\d+$")
|
|
37
|
+
|
|
38
|
+
_TRACE_FINDINGS = {
|
|
39
|
+
"unknown_scenario": ("unknown_scenario", "scenario-source", "coverage_ids", "Coverage 引用了场景源中不存在的场景"),
|
|
40
|
+
"scenario_not_in_coverage": ("scenario_not_in_coverage", "scenario-source", "coverage_ids", "场景源场景未进入 Acceptance Coverage"),
|
|
41
|
+
"duplicate_scenario_owner": ("duplicate_scenario_owner", "scenario-source", "unique_owner", "同一场景存在多个 owner"),
|
|
42
|
+
"missing_check": ("missing_check", "scenario-source", "checks_exist", "场景引用了不存在的 check"),
|
|
43
|
+
"design_scenario_duplicate": ("design_scenario_duplicate", "scenario-source", "design_references_only", "design 仍重复定义验收场景"),
|
|
44
|
+
"missing_in_manifest": (
|
|
45
|
+
"design_scenario_missing_in_manifest",
|
|
46
|
+
"RULE-10",
|
|
47
|
+
"design_manifest_consistency",
|
|
48
|
+
"design 已声明但 manifest 未登记",
|
|
49
|
+
),
|
|
50
|
+
"extra_in_manifest": (
|
|
51
|
+
"manifest_scenario_missing_in_design",
|
|
52
|
+
"RULE-10",
|
|
53
|
+
"design_manifest_consistency",
|
|
54
|
+
"manifest 已登记但 design 未声明",
|
|
55
|
+
),
|
|
56
|
+
"unbound_scenario": (
|
|
57
|
+
"scenario_unbound",
|
|
58
|
+
"RULE-02",
|
|
59
|
+
"scenario_binding",
|
|
60
|
+
"场景没有可执行 check,且未显式标为 aspirational",
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _section_body(text: str, heading: str) -> str:
|
|
66
|
+
match = re.search(rf"(?m)^(#{{2,4}})\s+{heading}(?:\s+[^\n]*)?$", text)
|
|
67
|
+
if match is None:
|
|
68
|
+
return ""
|
|
69
|
+
level = len(match.group(1))
|
|
70
|
+
tail = text[match.end():]
|
|
71
|
+
end = re.search(rf"(?m)^#{{1,{level}}}\s+", tail)
|
|
72
|
+
return tail[:end.start()] if end else tail
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _first_table(text: str) -> tuple[list[str], list[list[str]]]:
|
|
76
|
+
header: Optional[list[str]] = None
|
|
77
|
+
rows: list[list[str]] = []
|
|
78
|
+
for line in text.splitlines():
|
|
79
|
+
cells = table_cells(line)
|
|
80
|
+
if not cells:
|
|
81
|
+
if header is not None:
|
|
82
|
+
break
|
|
83
|
+
continue
|
|
84
|
+
if is_separator_row(cells):
|
|
85
|
+
continue
|
|
86
|
+
if header is None:
|
|
87
|
+
header = list(cells)
|
|
88
|
+
else:
|
|
89
|
+
rows.append(list(cells))
|
|
90
|
+
return header or [], rows
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cell(row: Sequence[str], index: int) -> str:
|
|
94
|
+
return row[index].strip() if 0 <= index < len(row) else ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_design_rules(design_file: str) -> list[dict[str, object]]:
|
|
98
|
+
"""Read RULE-* rows from design §2.5.1 (IDs and verified scenarios)."""
|
|
99
|
+
text = Path(design_file).read_text(encoding="utf-8")
|
|
100
|
+
header, rows = _first_table(_section_body(text, r"2\.5\.1"))
|
|
101
|
+
id_index = column_index(header, "id")
|
|
102
|
+
description_index = column_index(header, "描述", "description")
|
|
103
|
+
scenario_index = column_index(header, "验证场景", "场景")
|
|
104
|
+
rules: list[dict[str, object]] = []
|
|
105
|
+
for row in rows:
|
|
106
|
+
rule_id = _cell(row, id_index) if id_index >= 0 else _cell(row, 0)
|
|
107
|
+
if not _RULE_RE.match(rule_id):
|
|
108
|
+
continue
|
|
109
|
+
rules.append({
|
|
110
|
+
"id": rule_id,
|
|
111
|
+
"description": _cell(row, description_index),
|
|
112
|
+
"scenarios": _SCENARIO_RE.findall(_cell(row, scenario_index)),
|
|
113
|
+
})
|
|
114
|
+
return rules
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_design_risks(design_file: str) -> list[dict[str, object]]:
|
|
118
|
+
"""Read §5 risk rows; risk IDs are synthesized from their derived order."""
|
|
119
|
+
text = Path(design_file).read_text(encoding="utf-8")
|
|
120
|
+
header, rows = _first_table(_section_body(text, r"5\."))
|
|
121
|
+
level_index = column_index(header, "等级", "level")
|
|
122
|
+
risks: list[dict[str, object]] = []
|
|
123
|
+
for index, row in enumerate(rows):
|
|
124
|
+
title = _cell(row, 0)
|
|
125
|
+
if not title or title in {"风险", "-"}:
|
|
126
|
+
continue
|
|
127
|
+
risks.append({
|
|
128
|
+
"id": f"RISK-{index + 1:02d}",
|
|
129
|
+
"title": title,
|
|
130
|
+
"level": _cell(row, level_index) if level_index >= 0 else _cell(row, 1),
|
|
131
|
+
"details": {name: _cell(row, column) for column, name in enumerate(header)},
|
|
132
|
+
})
|
|
133
|
+
return risks
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _finding(code: str, scenario_id: str, rule_ref: str, invariant: str,
|
|
137
|
+
message: str, **extra: object) -> dict[str, object]:
|
|
138
|
+
item: dict[str, object] = {
|
|
139
|
+
"code": code,
|
|
140
|
+
"scenario_id": scenario_id,
|
|
141
|
+
"rule_ref": rule_ref,
|
|
142
|
+
"invariant": invariant,
|
|
143
|
+
"message": message,
|
|
144
|
+
}
|
|
145
|
+
item.update(extra)
|
|
146
|
+
return item
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _trace_findings(trace: Mapping[str, object]) -> list[dict[str, object]]:
|
|
150
|
+
findings: list[dict[str, object]] = []
|
|
151
|
+
for violation in trace.get("violations") or ():
|
|
152
|
+
if not isinstance(violation, Mapping):
|
|
153
|
+
continue
|
|
154
|
+
known = _TRACE_FINDINGS.get(str(violation.get("code")))
|
|
155
|
+
if known is None:
|
|
156
|
+
continue
|
|
157
|
+
code, rule_ref, invariant, message = known
|
|
158
|
+
findings.append(_finding(
|
|
159
|
+
code,
|
|
160
|
+
str(violation.get("scenario") or ""),
|
|
161
|
+
rule_ref,
|
|
162
|
+
invariant,
|
|
163
|
+
f"{message}: {violation.get('message', '')}",
|
|
164
|
+
))
|
|
165
|
+
return findings
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _owner_findings(scenarios: Mapping[str, Mapping[str, object]]) -> list[dict[str, object]]:
|
|
169
|
+
"""Each scenario must declare exactly one owner in its manifest field."""
|
|
170
|
+
findings: list[dict[str, object]] = []
|
|
171
|
+
for scenario_id in sorted(scenarios):
|
|
172
|
+
scenario = scenarios[scenario_id]
|
|
173
|
+
owner = scenario.get("owner")
|
|
174
|
+
if not isinstance(owner, str) or not owner.strip():
|
|
175
|
+
findings.append(_finding(
|
|
176
|
+
"owner_missing", scenario_id, "RULE-10", "owner_unique",
|
|
177
|
+
f"{scenario_id} 的 owner 字段为空;请登记唯一负责任务。",
|
|
178
|
+
fields=["owner"],
|
|
179
|
+
))
|
|
180
|
+
continue
|
|
181
|
+
tokens = re.findall(r"TASK-\d+", owner)
|
|
182
|
+
if len(tokens) != 1 or _TASK_RE.match(owner.strip()) is None:
|
|
183
|
+
findings.append(_finding(
|
|
184
|
+
"owner_not_unique", scenario_id, "RULE-10", "owner_unique",
|
|
185
|
+
f"{scenario_id} 的 owner 字段必须是单个 TASK ID,当前为 {owner!r}。",
|
|
186
|
+
fields=["owner"],
|
|
187
|
+
))
|
|
188
|
+
return findings
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _scenario_evidence(scenario: Mapping[str, object], store: EvidenceStore
|
|
192
|
+
) -> list[tuple[str, Mapping[str, object], str]]:
|
|
193
|
+
"""Resolve every required check; partial sidecars cannot borrow a pass."""
|
|
194
|
+
ids = scenario_check_ids(scenario)
|
|
195
|
+
sidecars = {check_id: store.read_latest(check_id) for check_id in ids}
|
|
196
|
+
if any(item is not None for item in sidecars.values()):
|
|
197
|
+
return [(check_id, item or {}, "sidecar" if item is not None else "missing")
|
|
198
|
+
for check_id, item in sidecars.items()]
|
|
199
|
+
value = scenario.get("evidence")
|
|
200
|
+
evidence = value if isinstance(value, Mapping) else {}
|
|
201
|
+
if not ids:
|
|
202
|
+
return [("", evidence, "manifest" if evidence else "missing")]
|
|
203
|
+
return [(check_id, {**check_outcome(evidence, check_id),
|
|
204
|
+
"input_hash": recorded_check_hash(scenario, check_id)},
|
|
205
|
+
"manifest" if evidence else "missing") for check_id in ids]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _evidence_fields(evidence: Mapping[str, object], expected_hash: str) -> list[str]:
|
|
209
|
+
"""Check the recorded proof, not the current workspace of a historical audit."""
|
|
210
|
+
missing: list[str] = []
|
|
211
|
+
if not isinstance(evidence.get("run_id"), str) or not str(evidence["run_id"]).strip():
|
|
212
|
+
missing.append("run_id")
|
|
213
|
+
if type(evidence.get("exit_code")) is not int or evidence["exit_code"] != 0:
|
|
214
|
+
missing.append("exit_code")
|
|
215
|
+
# An explicit empty per-check hash must not fall back to the scenario hash.
|
|
216
|
+
input_hash = evidence.get("input_hash", evidence.get("fingerprint"))
|
|
217
|
+
if not isinstance(input_hash, str) or not input_hash.strip():
|
|
218
|
+
missing.append("input_hash")
|
|
219
|
+
elif expected_hash and input_hash != expected_hash:
|
|
220
|
+
missing.append("input_hash_mismatch")
|
|
221
|
+
if evidence.get("status") not in ("passed", "verified"):
|
|
222
|
+
missing.append("status")
|
|
223
|
+
return missing
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _verified_findings(scenarios: Mapping[str, Mapping[str, object]],
|
|
227
|
+
store: EvidenceStore) -> list[dict[str, object]]:
|
|
228
|
+
findings: list[dict[str, object]] = []
|
|
229
|
+
for scenario_id in sorted(scenarios):
|
|
230
|
+
scenario = scenarios[scenario_id]
|
|
231
|
+
if scenario.get("status") != "verified":
|
|
232
|
+
continue
|
|
233
|
+
if scenario.get("kind") == "manual" or scenario.get("exec") == "manual":
|
|
234
|
+
if not verified_evidence(scenario):
|
|
235
|
+
findings.append(_finding(
|
|
236
|
+
"manual_verified_evidence_missing", scenario_id, "RULE-01",
|
|
237
|
+
"verified_evidence", f"{scenario_id} 手写 verified 但缺少用户确认记录。",
|
|
238
|
+
fields=["confirmed_by", "evidence"],
|
|
239
|
+
))
|
|
240
|
+
continue
|
|
241
|
+
for check_id, evidence, source in _scenario_evidence(scenario, store):
|
|
242
|
+
missing = _evidence_fields(evidence, recorded_check_hash(scenario, check_id))
|
|
243
|
+
if not missing:
|
|
244
|
+
continue
|
|
245
|
+
findings.append(_finding(
|
|
246
|
+
"verified_evidence_incomplete", scenario_id, "RULE-01",
|
|
247
|
+
"verified_evidence",
|
|
248
|
+
f"{scenario_id}/{check_id or 'scenario'} 已 verified,但证据无效: {', '.join(missing)}。",
|
|
249
|
+
check_id=check_id,
|
|
250
|
+
source=source,
|
|
251
|
+
fields=missing,
|
|
252
|
+
))
|
|
253
|
+
return findings
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _scenario_rows(trace: Mapping[str, object],
|
|
257
|
+
scenarios: Mapping[str, Mapping[str, object]]) -> list[dict[str, object]]:
|
|
258
|
+
rows: list[dict[str, object]] = []
|
|
259
|
+
for item in trace.get("scenarios") or ():
|
|
260
|
+
if not isinstance(item, Mapping):
|
|
261
|
+
continue
|
|
262
|
+
scenario_id = str(item.get("id") or "")
|
|
263
|
+
scenario = scenarios.get(scenario_id)
|
|
264
|
+
owner = str(scenario.get("owner") or "") if scenario is not None else str(item.get("owner") or "")
|
|
265
|
+
rows.append({
|
|
266
|
+
"id": scenario_id,
|
|
267
|
+
"owner": owner,
|
|
268
|
+
"status": str(scenario.get("status") or item.get("status") or "") if scenario is not None else "",
|
|
269
|
+
"check": next(iter(scenario_check_ids(scenario)), "-") if scenario is not None else "-",
|
|
270
|
+
"checks": scenario_check_ids(scenario) if scenario is not None else [],
|
|
271
|
+
"binding": str(item.get("binding") or ""),
|
|
272
|
+
})
|
|
273
|
+
return rows
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _dedupe(findings: Sequence[Mapping[str, object]]) -> list[dict[str, object]]:
|
|
277
|
+
unique: dict[tuple[str, str, str], dict[str, object]] = {}
|
|
278
|
+
for item in findings:
|
|
279
|
+
key = (str(item.get("scenario_id", "")), str(item.get("code", "")), str(item.get("message", "")))
|
|
280
|
+
unique[key] = dict(item)
|
|
281
|
+
return sorted(unique.values(), key=lambda item: (
|
|
282
|
+
str(item.get("scenario_id", "")), str(item.get("code", "")), str(item.get("message", ""))
|
|
283
|
+
))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _scenario_source_findings(scenarios_file: str, manifest_path: Path,
|
|
287
|
+
design_paths: Sequence[Path], manifest: Mapping[str, object]) -> list[dict[str, str]]:
|
|
288
|
+
from codeflow.cli.cf_scenarios import audit_scenario_source, parse_scenarios
|
|
289
|
+
from codeflow.acceptance.cf_traceability import parse_design_scenarios
|
|
290
|
+
source = parse_scenarios(scenarios_file)
|
|
291
|
+
coverage: list[dict[str, object]] = []
|
|
292
|
+
task_name = manifest.get("task_file")
|
|
293
|
+
if isinstance(task_name, str) and task_name:
|
|
294
|
+
task = manifest_path.parent / task_name
|
|
295
|
+
try:
|
|
296
|
+
from codeflow.cli.cf_acceptance_manifest import extract_manifest
|
|
297
|
+
coverage = list(extract_manifest(str(task))["scenarios"])
|
|
298
|
+
except (OSError, ValueError):
|
|
299
|
+
coverage = []
|
|
300
|
+
manifest_ids = [str(item.get("id")) for item in manifest.get("scenarios", []) if isinstance(item, Mapping)]
|
|
301
|
+
report = audit_scenario_source(source.get("scenarios", []), coverage, manifest_ids)
|
|
302
|
+
violations: list[dict[str, str]] = []
|
|
303
|
+
for code, key in (("unknown_scenario", "unknown"), ("scenario_not_in_coverage", "missing"),
|
|
304
|
+
("duplicate_scenario_owner", "duplicate_owner"),
|
|
305
|
+
("missing_in_manifest", "manifest_missing"),
|
|
306
|
+
("extra_in_manifest", "manifest_unknown")):
|
|
307
|
+
for scenario_id in report.get(key, []):
|
|
308
|
+
violations.append({"code": code, "scenario": str(scenario_id), "message": f"{scenario_id}: {key}"})
|
|
309
|
+
paths = [design_paths] if isinstance(design_paths, Path) else list(design_paths)
|
|
310
|
+
design_ids: set[str] = set()
|
|
311
|
+
for path in paths:
|
|
312
|
+
try:
|
|
313
|
+
design_ids.update(item.id for item in parse_design_scenarios(str(path)))
|
|
314
|
+
except (OSError, ValueError):
|
|
315
|
+
continue
|
|
316
|
+
for scenario_id in sorted(design_ids):
|
|
317
|
+
violations.append({"code": "design_scenario_duplicate", "scenario": scenario_id,
|
|
318
|
+
"message": f"{scenario_id} 仍在 design 中定义;场景源已接管"})
|
|
319
|
+
return violations
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _merge_design_rules(design_paths: Sequence[Path]) -> list[dict[str, object]]:
|
|
323
|
+
merged: dict[str, dict[str, object]] = {}
|
|
324
|
+
for path in design_paths:
|
|
325
|
+
for rule in parse_design_rules(str(path)):
|
|
326
|
+
rule_id = str(rule.get("id"))
|
|
327
|
+
if rule_id in merged and merged[rule_id] != rule:
|
|
328
|
+
raise ValueError(f"duplicate design rule id with conflicting definition: {rule_id}")
|
|
329
|
+
merged[rule_id] = rule
|
|
330
|
+
return sorted(merged.values(), key=lambda item: str(item.get("id")))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _merge_design_risks(design_paths: Sequence[Path]) -> list[dict[str, object]]:
|
|
334
|
+
merged: list[dict[str, object]] = []
|
|
335
|
+
seen: set[tuple[str, str]] = set()
|
|
336
|
+
for path in design_paths:
|
|
337
|
+
for risk in parse_design_risks(str(path)):
|
|
338
|
+
key = (str(path.resolve()), str(risk["id"]))
|
|
339
|
+
if key in seen:
|
|
340
|
+
continue
|
|
341
|
+
seen.add(key)
|
|
342
|
+
item = dict(risk)
|
|
343
|
+
item["source"] = path.name
|
|
344
|
+
item["source_ref"] = f"{path.resolve().as_posix()}#{risk['id']}"
|
|
345
|
+
merged.append(item)
|
|
346
|
+
return merged
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _audit_summary(trace: Mapping[str, object], manifest: Mapping[str, object],
|
|
350
|
+
rows: Sequence[Mapping[str, object]], rules: Sequence[Mapping[str, object]],
|
|
351
|
+
risks: Sequence[Mapping[str, object]], findings: Sequence[Mapping[str, object]]) -> dict[str, object]:
|
|
352
|
+
owners: dict[str, int] = {}
|
|
353
|
+
for row in rows:
|
|
354
|
+
owner = str(row.get("owner") or "")
|
|
355
|
+
if owner:
|
|
356
|
+
owners[owner] = owners.get(owner, 0) + 1
|
|
357
|
+
risk_levels: dict[str, int] = {}
|
|
358
|
+
for risk in risks:
|
|
359
|
+
level = str(risk.get("level") or "unknown")
|
|
360
|
+
risk_levels[level] = risk_levels.get(level, 0) + 1
|
|
361
|
+
summary = dict(trace.get("summary") or {})
|
|
362
|
+
summary.update({
|
|
363
|
+
"checks": len(manifest.get("checks", [])),
|
|
364
|
+
"verified": sum(1 for item in manifest.get("scenarios", []) if item.get("status") == "verified"),
|
|
365
|
+
"owners": owners,
|
|
366
|
+
"unique_owners": len(owners),
|
|
367
|
+
"rules": len(rules),
|
|
368
|
+
"risks": len(risks),
|
|
369
|
+
**risk_counts(manifest),
|
|
370
|
+
"risk_levels": risk_levels,
|
|
371
|
+
"findings": len(findings),
|
|
372
|
+
})
|
|
373
|
+
return summary
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _audit_trace(design_paths: Sequence[Path], manifest_path: Path,
|
|
377
|
+
manifest: Mapping[str, object], scenarios_file: str) -> dict[str, object]:
|
|
378
|
+
if scenarios_file:
|
|
379
|
+
trace = build_traceability_from_scenarios(scenarios_file, str(manifest_path))
|
|
380
|
+
extra_findings = _scenario_source_findings(scenarios_file, manifest_path, design_paths, manifest)
|
|
381
|
+
trace.setdefault("violations", []).extend(extra_findings)
|
|
382
|
+
return trace
|
|
383
|
+
if len(design_paths) > 1:
|
|
384
|
+
return build_traceability_many([str(path) for path in design_paths], str(manifest_path))
|
|
385
|
+
return build_traceability(str(design_paths[0]), str(manifest_path))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def build_audit(design_file: str, manifest_file: str, root: str,
|
|
389
|
+
requirement: str = "", scenarios_file: str = "",
|
|
390
|
+
design_files: Sequence[str] = ()) -> dict[str, object]:
|
|
391
|
+
"""Build the read-only invariant report; multiple designs merge by global ID."""
|
|
392
|
+
design_paths = [Path(item) for item in design_files] or [Path(design_file)]
|
|
393
|
+
manifest_path = Path(manifest_file)
|
|
394
|
+
if any(not path.is_file() for path in design_paths):
|
|
395
|
+
raise ValueError(f"design_file_missing: {', '.join(str(path) for path in design_paths)}")
|
|
396
|
+
if not manifest_path.is_file():
|
|
397
|
+
raise ValueError(f"manifest_file_missing: {manifest_path}")
|
|
398
|
+
rules = _merge_design_rules(design_paths)
|
|
399
|
+
risks = _merge_design_risks(design_paths)
|
|
400
|
+
manifest = load_manifest(manifest_path)
|
|
401
|
+
trace = _audit_trace(design_paths, manifest_path, manifest, scenarios_file)
|
|
402
|
+
scenarios = {
|
|
403
|
+
str(item["id"]): item
|
|
404
|
+
for item in manifest.get("scenarios") or ()
|
|
405
|
+
if isinstance(item, Mapping) and isinstance(item.get("id"), str)
|
|
406
|
+
}
|
|
407
|
+
from codeflow.evidence.cf_evidence_store import requirement_key
|
|
408
|
+
store = EvidenceStore(str(root), requirement or requirement_key(manifest_path.parent))
|
|
409
|
+
findings = [
|
|
410
|
+
*_trace_findings(trace),
|
|
411
|
+
*_owner_findings(scenarios),
|
|
412
|
+
*_verified_findings(scenarios, store),
|
|
413
|
+
]
|
|
414
|
+
findings = _dedupe(findings)
|
|
415
|
+
rows = _scenario_rows(trace, scenarios)
|
|
416
|
+
summary = _audit_summary(trace, manifest, rows, rules, risks, findings)
|
|
417
|
+
return {
|
|
418
|
+
"decision": "block" if findings else "pass",
|
|
419
|
+
"read_only": True,
|
|
420
|
+
"generated": True,
|
|
421
|
+
"design_file": str(design_paths[0]),
|
|
422
|
+
"design_files": [str(path) for path in design_paths],
|
|
423
|
+
"source": trace.get("source", ""),
|
|
424
|
+
"deprecated": not bool(scenarios_file),
|
|
425
|
+
"manifest_file": str(manifest_path),
|
|
426
|
+
"evidence_dir": str(store.directory),
|
|
427
|
+
"summary": summary,
|
|
428
|
+
"rules": rules,
|
|
429
|
+
"risks": risks,
|
|
430
|
+
"scenarios": rows,
|
|
431
|
+
"findings": findings,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _md(value: object) -> str:
|
|
436
|
+
text = "" if value is None else str(value)
|
|
437
|
+
if not text:
|
|
438
|
+
return "-"
|
|
439
|
+
return text.replace("|", "\\|").replace("\n", " ")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def render_markdown(report: Mapping[str, object]) -> str:
|
|
443
|
+
"""Render a human report from build_audit output; no file is written."""
|
|
444
|
+
summary = report.get("summary") if isinstance(report.get("summary"), Mapping) else {}
|
|
445
|
+
lines = [
|
|
446
|
+
"# code-flow audit",
|
|
447
|
+
"",
|
|
448
|
+
f"> Read-only audit of `{_md(report.get('manifest_file'))}` against "
|
|
449
|
+
f"`{_md(report.get('design_file'))}`.",
|
|
450
|
+
"",
|
|
451
|
+
"| Metric | Value |",
|
|
452
|
+
"|--------|-------|",
|
|
453
|
+
f"| decision | {_md(report.get('decision'))} |",
|
|
454
|
+
f"| design scenarios | {_md(summary.get('design_scenarios'))} |",
|
|
455
|
+
f"| manifest scenarios | {_md(summary.get('manifest_scenarios'))} |",
|
|
456
|
+
f"| checks | {_md(summary.get('checks'))} |",
|
|
457
|
+
f"| bound / aspirational / unbound / missing | "
|
|
458
|
+
f"{_md(summary.get('bound'))} / {_md(summary.get('aspirational'))} / "
|
|
459
|
+
f"{_md(summary.get('unbound'))} / {_md(summary.get('missing'))} |",
|
|
460
|
+
f"| verified | {_md(summary.get('verified'))} |",
|
|
461
|
+
f"| rules | {_md(summary.get('rules'))} |",
|
|
462
|
+
f"| risks | {_md(summary.get('risks'))} |",
|
|
463
|
+
f"| accepted risks | {_md(summary.get('accepted_risks'))} |",
|
|
464
|
+
f"| findings | {_md(summary.get('findings'))} |",
|
|
465
|
+
]
|
|
466
|
+
findings = report.get("findings") or ()
|
|
467
|
+
if findings:
|
|
468
|
+
lines.extend([
|
|
469
|
+
"",
|
|
470
|
+
"## Findings",
|
|
471
|
+
"",
|
|
472
|
+
"| Rule | Invariant | Scenario | Code | Message |",
|
|
473
|
+
"|------|-----------|----------|------|---------|",
|
|
474
|
+
])
|
|
475
|
+
for item in findings:
|
|
476
|
+
if not isinstance(item, Mapping):
|
|
477
|
+
continue
|
|
478
|
+
lines.append(
|
|
479
|
+
f"| {_md(item.get('rule_ref'))} | {_md(item.get('invariant'))} | "
|
|
480
|
+
f"{_md(item.get('scenario_id'))} | {_md(item.get('code'))} | {_md(item.get('message'))} |"
|
|
481
|
+
)
|
|
482
|
+
else:
|
|
483
|
+
lines.extend(["", "No invariant findings."])
|
|
484
|
+
return "\n".join(lines) + "\n"
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _resolve_paths(root: str, task_dir: str, design: str,
|
|
488
|
+
manifest: str) -> tuple[Path, Path, Path, list[Path]]:
|
|
489
|
+
root_path = Path(root).expanduser().resolve()
|
|
490
|
+
directory = Path(task_dir).expanduser()
|
|
491
|
+
directory = directory.resolve() if directory.is_absolute() else (root_path / directory).resolve()
|
|
492
|
+
if design:
|
|
493
|
+
design_paths = [
|
|
494
|
+
Path(item).expanduser().resolve()
|
|
495
|
+
for item in design.split(",") if item.strip()
|
|
496
|
+
]
|
|
497
|
+
else:
|
|
498
|
+
design_paths = sorted(directory.glob("*.design.md"))
|
|
499
|
+
if not design_paths:
|
|
500
|
+
raise ValueError(f"design_file_missing: {directory}")
|
|
501
|
+
manifest_path = Path(manifest).expanduser().resolve() if manifest else directory / ".acceptance-manifest.json"
|
|
502
|
+
return root_path, design_paths[0], manifest_path, design_paths
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
506
|
+
parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND") or None)
|
|
507
|
+
parser.add_argument("--task-dir", required=True)
|
|
508
|
+
parser.add_argument("--root", default=os.getcwd())
|
|
509
|
+
parser.add_argument("--design", default="")
|
|
510
|
+
parser.add_argument("--manifest", default="")
|
|
511
|
+
parser.add_argument("--json", action="store_true")
|
|
512
|
+
args = parser.parse_args(argv)
|
|
513
|
+
try:
|
|
514
|
+
root, design, manifest, design_paths = _resolve_paths(
|
|
515
|
+
args.root, args.task_dir, args.design, args.manifest
|
|
516
|
+
)
|
|
517
|
+
resolved_dir = manifest.parent
|
|
518
|
+
sources = sorted(resolved_dir.glob("*.scenarios.md")) if resolved_dir.is_dir() else []
|
|
519
|
+
scenarios_file = str(sources[0]) if len(sources) == 1 else ""
|
|
520
|
+
if len(sources) > 1:
|
|
521
|
+
raise ValueError("scenario_source_ambiguous")
|
|
522
|
+
report = build_audit(
|
|
523
|
+
str(design), str(manifest), root=str(root), scenarios_file=scenarios_file,
|
|
524
|
+
design_files=[str(path) for path in design_paths],
|
|
525
|
+
)
|
|
526
|
+
except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as exc:
|
|
527
|
+
stdout.write(json.dumps({"decision": "block", "error": str(exc)}, ensure_ascii=False))
|
|
528
|
+
return 2
|
|
529
|
+
if args.json:
|
|
530
|
+
stdout.write(json.dumps(report, ensure_ascii=False))
|
|
531
|
+
else:
|
|
532
|
+
stdout.write(render_markdown(report))
|
|
533
|
+
return 0 if report["decision"] == "pass" else 3
|