@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,502 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate the requirement traceability matrix from design §2.5 + manifest.
|
|
3
|
+
|
|
4
|
+
The matrix is a derived view, never hand-edited: every design scenario is joined
|
|
5
|
+
with its manifest scenario/check so priority, exec mode, env, inputs, command,
|
|
6
|
+
owner and status all come from data. A scenario without a check is only allowed
|
|
7
|
+
when it is explicitly marked ``aspirational``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from typing import IO, Mapping, Optional, Sequence
|
|
19
|
+
|
|
20
|
+
if __package__ in (None, ""):
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
22
|
+
|
|
23
|
+
from codeflow.acceptance.cf_acceptance_schema import EXEC_MODES, load_manifest, scenario_check_ids
|
|
24
|
+
from codeflow.core.cf_markdown import column_index, is_separator_row, table_cells
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_SCENARIO_ID_RE = re.compile(r"^[SEB]-\d+$")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class DesignScenario:
|
|
32
|
+
"""One scenario declared in design §2.5, independent of the manifest."""
|
|
33
|
+
|
|
34
|
+
id: str
|
|
35
|
+
feature: str
|
|
36
|
+
priority: str
|
|
37
|
+
level: str
|
|
38
|
+
boundary: str
|
|
39
|
+
exec: str
|
|
40
|
+
env: str
|
|
41
|
+
inputs: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _acceptance_section(text: str) -> str:
|
|
45
|
+
match = re.search(r"(?m)^(#{2,4})\s+2\.5(?:\s+[^\n]*)?$", text)
|
|
46
|
+
if match is None:
|
|
47
|
+
raise ValueError("design §2.5 验收条件缺失")
|
|
48
|
+
level = len(match.group(1))
|
|
49
|
+
tail = text[match.end():]
|
|
50
|
+
end = re.search(rf"(?m)^#{{1,{level}}}\s+", tail)
|
|
51
|
+
return tail[:end.start()] if end else tail
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _table_blocks(text: str) -> list[tuple[list[str], list[list[str]]]]:
|
|
55
|
+
blocks: list[tuple[list[str], list[list[str]]]] = []
|
|
56
|
+
header: Optional[list[str]] = None
|
|
57
|
+
rows: list[list[str]] = []
|
|
58
|
+
for line in text.splitlines():
|
|
59
|
+
cells = table_cells(line)
|
|
60
|
+
if cells:
|
|
61
|
+
if is_separator_row(cells):
|
|
62
|
+
continue
|
|
63
|
+
if header is None:
|
|
64
|
+
header = list(cells)
|
|
65
|
+
else:
|
|
66
|
+
rows.append(list(cells))
|
|
67
|
+
continue
|
|
68
|
+
if header is not None:
|
|
69
|
+
blocks.append((header, rows))
|
|
70
|
+
header, rows = None, []
|
|
71
|
+
if header is not None:
|
|
72
|
+
blocks.append((header, rows))
|
|
73
|
+
return blocks
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cell(row: Sequence[str], index: int) -> str:
|
|
77
|
+
return row[index].strip() if 0 <= index < len(row) else ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _design_exec(value: str, scenario_id: str) -> str:
|
|
81
|
+
text = value.strip().strip("`")
|
|
82
|
+
if not text or text in {"-", "N/A"}:
|
|
83
|
+
return ""
|
|
84
|
+
normalized = text.lower()
|
|
85
|
+
if normalized not in EXEC_MODES:
|
|
86
|
+
raise ValueError(f"{scenario_id} 执行方式非法: {value}(允许 {', '.join(EXEC_MODES)})")
|
|
87
|
+
return normalized
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _require_full_design_scenarios(text: str) -> None:
|
|
91
|
+
"""Only complete legacy definitions can substitute for a missing source."""
|
|
92
|
+
references = False
|
|
93
|
+
for header, rows in _table_blocks(text):
|
|
94
|
+
column = column_index(header, "场景id", "场景编号")
|
|
95
|
+
if column < 0 or not any(_SCENARIO_ID_RE.match(_cell(row, column)) for row in rows):
|
|
96
|
+
continue
|
|
97
|
+
if column_index(header, "测试层级") >= 0:
|
|
98
|
+
return
|
|
99
|
+
references = True
|
|
100
|
+
if references:
|
|
101
|
+
raise ValueError("scenario_source_missing: reference-only design requires the original .scenarios.md; "
|
|
102
|
+
"restore the scenario source before audit")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_design_scenarios(design_file: str) -> tuple[DesignScenario, ...]:
|
|
106
|
+
"""Parse all S-/E-/B- rows under design §2.5 tables."""
|
|
107
|
+
try:
|
|
108
|
+
text = Path(design_file).read_text(encoding="utf-8")
|
|
109
|
+
except (OSError, UnicodeError) as exc:
|
|
110
|
+
raise ValueError(f"design_read_error: {exc}") from exc
|
|
111
|
+
_require_full_design_scenarios(text)
|
|
112
|
+
section = _acceptance_section(text)
|
|
113
|
+
scenarios: dict[str, DesignScenario] = {}
|
|
114
|
+
for header, rows in _table_blocks(section):
|
|
115
|
+
id_index = column_index(header, "场景id", "场景编号")
|
|
116
|
+
level_index = column_index(header, "测试层级")
|
|
117
|
+
if id_index == -1 or level_index == -1:
|
|
118
|
+
continue
|
|
119
|
+
feature_index = column_index(header, "功能id", "feature")
|
|
120
|
+
priority_index = column_index(header, "优先级", "priority")
|
|
121
|
+
boundary_index = column_index(header, "关键真实边界", "真实边界")
|
|
122
|
+
exec_index = column_index(header, "执行方式", "exec")
|
|
123
|
+
env_index = column_index(header, "环境", "env")
|
|
124
|
+
inputs_index = column_index(header, "输入", "inputs")
|
|
125
|
+
for row in rows:
|
|
126
|
+
scenario_id = _cell(row, id_index)
|
|
127
|
+
if not _SCENARIO_ID_RE.match(scenario_id):
|
|
128
|
+
continue
|
|
129
|
+
if scenario_id in scenarios:
|
|
130
|
+
raise ValueError(f"duplicate design scenario id: {scenario_id}")
|
|
131
|
+
scenarios[scenario_id] = DesignScenario(
|
|
132
|
+
id=scenario_id,
|
|
133
|
+
feature=_cell(row, feature_index),
|
|
134
|
+
priority=_cell(row, priority_index).upper(),
|
|
135
|
+
level=_cell(row, level_index),
|
|
136
|
+
boundary=_cell(row, boundary_index),
|
|
137
|
+
exec=_design_exec(_cell(row, exec_index), scenario_id),
|
|
138
|
+
env=_cell(row, env_index),
|
|
139
|
+
inputs=_cell(row, inputs_index),
|
|
140
|
+
)
|
|
141
|
+
if not scenarios:
|
|
142
|
+
raise ValueError("design §2.5 未找到 S-/E-/B- 场景")
|
|
143
|
+
return tuple(scenarios.values())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _load_manifest(manifest_file: str) -> tuple[Mapping[str, object], dict[str, Mapping[str, object]],
|
|
147
|
+
dict[str, Mapping[str, object]]]:
|
|
148
|
+
try:
|
|
149
|
+
data = load_manifest(Path(manifest_file))
|
|
150
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
151
|
+
raise ValueError(f"manifest_read_error: {exc}") from exc
|
|
152
|
+
scenarios = {
|
|
153
|
+
str(item["id"]): item
|
|
154
|
+
for item in data.get("scenarios") or ()
|
|
155
|
+
if isinstance(item, Mapping) and isinstance(item.get("id"), str)
|
|
156
|
+
}
|
|
157
|
+
checks = {
|
|
158
|
+
str(item["id"]): item
|
|
159
|
+
for item in data.get("checks") or ()
|
|
160
|
+
if isinstance(item, Mapping) and isinstance(item.get("id"), str)
|
|
161
|
+
}
|
|
162
|
+
return data, scenarios, checks
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _design_inputs(value: str) -> list[str]:
|
|
166
|
+
text = (value or "").strip()
|
|
167
|
+
if not text or text in {"-", "none", "N/A"}:
|
|
168
|
+
return []
|
|
169
|
+
return [item.strip() for item in re.split(r"[,,、;;]", text) if item.strip()]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _binding(scenario: Mapping[str, object], check: Optional[Mapping[str, object]],
|
|
173
|
+
design: DesignScenario) -> tuple[str, str, str]:
|
|
174
|
+
check_id = next(iter(scenario_check_ids(scenario)), "")
|
|
175
|
+
exec_mode = str(scenario.get("exec") or design.exec or "")
|
|
176
|
+
if isinstance(check_id, str) and check_id and check is not None:
|
|
177
|
+
return "check", check_id, exec_mode or "auto"
|
|
178
|
+
if exec_mode == "aspirational":
|
|
179
|
+
return "aspirational", "-", exec_mode
|
|
180
|
+
return "unbound", "-", exec_mode
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _row(design: DesignScenario, scenario: Optional[Mapping[str, object]],
|
|
184
|
+
check: Optional[Mapping[str, object]]) -> dict[str, object]:
|
|
185
|
+
if scenario is None:
|
|
186
|
+
return {
|
|
187
|
+
"id": design.id,
|
|
188
|
+
"feature": design.feature,
|
|
189
|
+
"priority": design.priority,
|
|
190
|
+
"level": design.level,
|
|
191
|
+
"boundary": design.boundary,
|
|
192
|
+
"exec": design.exec,
|
|
193
|
+
"env": design.env or "none",
|
|
194
|
+
"inputs": _design_inputs(design.inputs),
|
|
195
|
+
"check": "-",
|
|
196
|
+
"command": [],
|
|
197
|
+
"owner": "",
|
|
198
|
+
"status": "missing",
|
|
199
|
+
"binding": "missing",
|
|
200
|
+
"in_design": True,
|
|
201
|
+
}
|
|
202
|
+
binding, check_id, exec_mode = _binding(scenario, check, design)
|
|
203
|
+
command = check.get("command") if check else scenario.get("command")
|
|
204
|
+
inputs = check.get("inputs") if check else scenario.get("inputs") or _design_inputs(design.inputs)
|
|
205
|
+
return {
|
|
206
|
+
"id": design.id,
|
|
207
|
+
"feature": design.feature,
|
|
208
|
+
"priority": str(scenario.get("priority") or design.priority),
|
|
209
|
+
"level": str(scenario.get("level") or design.level),
|
|
210
|
+
"boundary": str(scenario.get("boundary") or design.boundary),
|
|
211
|
+
"exec": exec_mode or ("auto" if binding == "check" else ""),
|
|
212
|
+
"env": str(check.get("env", "none")) if check else str(scenario.get("env") or design.env or "none"),
|
|
213
|
+
"inputs": list(inputs) if isinstance(inputs, list) else _design_inputs(str(inputs or "")),
|
|
214
|
+
"check": check_id,
|
|
215
|
+
"checks": scenario_check_ids(scenario),
|
|
216
|
+
"command": list(command) if isinstance(command, list) else [],
|
|
217
|
+
"owner": str(scenario.get("owner") or ""),
|
|
218
|
+
"status": str(scenario.get("status") or ("aspirational" if binding == "aspirational" else "")),
|
|
219
|
+
"binding": binding,
|
|
220
|
+
"in_design": True,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _extra_rows(manifest_scenarios: Mapping[str, Mapping[str, object]],
|
|
225
|
+
checks: Mapping[str, Mapping[str, object]], design_ids: set[str]) -> list[dict[str, object]]:
|
|
226
|
+
rows: list[dict[str, object]] = []
|
|
227
|
+
for scenario_id in sorted(set(manifest_scenarios) - design_ids):
|
|
228
|
+
scenario = manifest_scenarios[scenario_id]
|
|
229
|
+
check_id = next(iter(scenario_check_ids(scenario)), "")
|
|
230
|
+
check = checks.get(check_id) if isinstance(check_id, str) else None
|
|
231
|
+
exec_mode = str(scenario.get("exec") or "")
|
|
232
|
+
binding = "check" if check is not None else ("aspirational" if exec_mode == "aspirational" else "unbound")
|
|
233
|
+
command = check.get("command") if check else scenario.get("command")
|
|
234
|
+
rows.append({
|
|
235
|
+
"id": scenario_id,
|
|
236
|
+
"feature": "",
|
|
237
|
+
"priority": str(scenario.get("priority") or ""),
|
|
238
|
+
"level": str(scenario.get("level") or ""),
|
|
239
|
+
"boundary": str(scenario.get("boundary") or ""),
|
|
240
|
+
"exec": exec_mode,
|
|
241
|
+
"env": str(check.get("env", "none")) if check else str(scenario.get("env") or "none"),
|
|
242
|
+
"inputs": list(check.get("inputs") or []) if check else list(scenario.get("inputs") or []),
|
|
243
|
+
"check": check_id if isinstance(check_id, str) and check is not None else "-",
|
|
244
|
+
"checks": scenario_check_ids(scenario),
|
|
245
|
+
"command": list(command) if isinstance(command, list) else [],
|
|
246
|
+
"owner": str(scenario.get("owner") or ""),
|
|
247
|
+
"status": str(scenario.get("status") or ""),
|
|
248
|
+
"binding": binding,
|
|
249
|
+
"in_design": False,
|
|
250
|
+
})
|
|
251
|
+
return rows
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _extra_violations(rows: Sequence[Mapping[str, object]]) -> list[dict[str, str]]:
|
|
255
|
+
violations: list[dict[str, str]] = []
|
|
256
|
+
for row in rows:
|
|
257
|
+
violations.append({
|
|
258
|
+
"code": "extra_in_manifest",
|
|
259
|
+
"scenario": str(row["id"]),
|
|
260
|
+
"message": f"{row['id']} 存在于 manifest,但 design §2.5 未声明;请回填设计或移除场景。",
|
|
261
|
+
})
|
|
262
|
+
if row["binding"] == "unbound":
|
|
263
|
+
violations.append({
|
|
264
|
+
"code": "unbound_scenario",
|
|
265
|
+
"scenario": str(row["id"]),
|
|
266
|
+
"message": f"{row['id']} 没有可执行 check,且未显式标为 aspirational。",
|
|
267
|
+
})
|
|
268
|
+
return violations
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _traceability_rows(
|
|
272
|
+
design: Sequence[DesignScenario], manifest_file: str, design_label: str
|
|
273
|
+
) -> dict[str, object]:
|
|
274
|
+
_, manifest_scenarios, checks = _load_manifest(manifest_file)
|
|
275
|
+
design_ids = {item.id for item in design}
|
|
276
|
+
violations: list[dict[str, str]] = []
|
|
277
|
+
rows: list[dict[str, object]] = []
|
|
278
|
+
for item in design:
|
|
279
|
+
scenario = manifest_scenarios.get(item.id)
|
|
280
|
+
check_id = next(iter(scenario_check_ids(scenario)), "") if scenario is not None else None
|
|
281
|
+
check = checks.get(check_id) if isinstance(check_id, str) else None
|
|
282
|
+
row = _row(item, scenario, check)
|
|
283
|
+
rows.append(row)
|
|
284
|
+
if scenario is None:
|
|
285
|
+
violations.append({
|
|
286
|
+
"code": "missing_in_manifest",
|
|
287
|
+
"scenario": item.id,
|
|
288
|
+
"message": f"{item.id} 在 design §2.5 中声明,但未进入 manifest;请 plan 覆盖该场景。",
|
|
289
|
+
})
|
|
290
|
+
elif row["binding"] == "unbound":
|
|
291
|
+
violations.append({
|
|
292
|
+
"code": "unbound_scenario",
|
|
293
|
+
"scenario": item.id,
|
|
294
|
+
"message": (
|
|
295
|
+
f"{item.id} 没有可执行 check,且未显式标为 aspirational;"
|
|
296
|
+
"请登记执行命令或把执行方式改为 aspirational。"
|
|
297
|
+
),
|
|
298
|
+
})
|
|
299
|
+
extras = _extra_rows(manifest_scenarios, checks, design_ids)
|
|
300
|
+
violations.extend(_extra_violations(extras))
|
|
301
|
+
all_rows = rows + extras
|
|
302
|
+
summary = {
|
|
303
|
+
"design_scenarios": len(design),
|
|
304
|
+
"manifest_scenarios": len(manifest_scenarios),
|
|
305
|
+
"bound": sum(1 for row in all_rows if row["binding"] == "check"),
|
|
306
|
+
"aspirational": sum(1 for row in all_rows if row["binding"] == "aspirational"),
|
|
307
|
+
"unbound": sum(1 for row in all_rows if row["binding"] == "unbound"),
|
|
308
|
+
"missing": sum(1 for row in all_rows if row["binding"] == "missing"),
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
"decision": "block" if violations else "pass",
|
|
312
|
+
"generated": True,
|
|
313
|
+
"design_file": design_label,
|
|
314
|
+
"manifest_file": str(manifest_file),
|
|
315
|
+
"summary": summary,
|
|
316
|
+
"scenarios": all_rows,
|
|
317
|
+
"violations": violations,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def build_traceability(design_file: str, manifest_file: str) -> dict[str, object]:
|
|
322
|
+
"""Join design §2.5 scenarios with manifest checks and report binding gaps."""
|
|
323
|
+
return _traceability_rows(parse_design_scenarios(design_file), manifest_file, str(design_file))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def merge_design_scenarios(design_files: Sequence[str]) -> tuple[DesignScenario, ...]:
|
|
327
|
+
"""Merge frontend/backend design scenarios by global ID; conflicts fail closed."""
|
|
328
|
+
merged: dict[str, DesignScenario] = {}
|
|
329
|
+
for design_file in design_files:
|
|
330
|
+
for item in parse_design_scenarios(design_file):
|
|
331
|
+
previous = merged.get(item.id)
|
|
332
|
+
if previous is not None and previous != item:
|
|
333
|
+
raise ValueError(f"duplicate design scenario id with conflicting definition: {item.id}")
|
|
334
|
+
merged[item.id] = item
|
|
335
|
+
if not merged:
|
|
336
|
+
raise ValueError("design §2.5 未找到 S-/E-/B- 场景")
|
|
337
|
+
return tuple(merged.values())
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def build_traceability_many(design_files: Sequence[str], manifest_file: str) -> dict[str, object]:
|
|
341
|
+
"""0.8 fallback: merge multiple design docs before joining the manifest."""
|
|
342
|
+
design = merge_design_scenarios(design_files)
|
|
343
|
+
return _traceability_rows(design, manifest_file, ", ".join(str(item) for item in design_files))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _md(value: object) -> str:
|
|
347
|
+
text = "" if value is None else str(value)
|
|
348
|
+
if not text:
|
|
349
|
+
return "-"
|
|
350
|
+
return text.replace("|", "\\|").replace("\n", " ")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _md_inputs(value: object) -> str:
|
|
354
|
+
if isinstance(value, list):
|
|
355
|
+
return _md(", ".join(str(item) for item in value))
|
|
356
|
+
return _md(value)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _md_command(value: object) -> str:
|
|
360
|
+
if isinstance(value, list):
|
|
361
|
+
return _md(json.dumps(value, ensure_ascii=False))
|
|
362
|
+
return _md(value)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def render_markdown(report: Mapping[str, object]) -> str:
|
|
366
|
+
"""Render the generated matrix; input is always build_traceability output."""
|
|
367
|
+
summary = report.get("summary") if isinstance(report.get("summary"), Mapping) else {}
|
|
368
|
+
lines = [
|
|
369
|
+
"> 由 `cf_traceability.py` 从 design §2.5 + manifest 生成,不手工维护。",
|
|
370
|
+
">",
|
|
371
|
+
(
|
|
372
|
+
f"> 场景 {summary.get('design_scenarios', 0)} / 绑定 check {summary.get('bound', 0)}"
|
|
373
|
+
f" / aspirational {summary.get('aspirational', 0)}"
|
|
374
|
+
f" / 未绑定 {summary.get('unbound', 0)} / 缺失 {summary.get('missing', 0)}"
|
|
375
|
+
),
|
|
376
|
+
"",
|
|
377
|
+
"| 场景ID | 优先级 | 测试层级 | 执行方式 | 环境 | 输入 | 追溯 check | 执行命令 | 负责任务 | 状态 |",
|
|
378
|
+
"|--------|--------|---------|---------|------|------|-----------|---------|---------|------|",
|
|
379
|
+
]
|
|
380
|
+
for row in report.get("scenarios") or ():
|
|
381
|
+
if not isinstance(row, Mapping):
|
|
382
|
+
continue
|
|
383
|
+
lines.append(
|
|
384
|
+
"| {id} | {priority} | {level} | {exec} | {env} | {inputs} | {check} | {command} | {owner} | {status} |".format(
|
|
385
|
+
id=_md(row.get("id")),
|
|
386
|
+
priority=_md(row.get("priority")),
|
|
387
|
+
level=_md(row.get("level")),
|
|
388
|
+
exec=_md(row.get("exec")),
|
|
389
|
+
env=_md(row.get("env")),
|
|
390
|
+
inputs=_md_inputs(row.get("inputs")),
|
|
391
|
+
check=_md(row.get("check")),
|
|
392
|
+
command=_md_command(row.get("command")),
|
|
393
|
+
owner=_md(row.get("owner")),
|
|
394
|
+
status=_md(row.get("status")),
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
if report.get("violations"):
|
|
398
|
+
lines.extend(["", "**阻断项**", ""])
|
|
399
|
+
for violation in report["violations"]:
|
|
400
|
+
if isinstance(violation, Mapping):
|
|
401
|
+
lines.append(f"- `{violation.get('code')}` {violation.get('scenario')}: {violation.get('message')}")
|
|
402
|
+
return "\n".join(lines) + "\n"
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
406
|
+
parser = argparse.ArgumentParser(prog="cf_traceability.py")
|
|
407
|
+
parser.add_argument("--design", required=True)
|
|
408
|
+
parser.add_argument("--manifest", required=True)
|
|
409
|
+
parser.add_argument("--json", action="store_true")
|
|
410
|
+
args = parser.parse_args(argv)
|
|
411
|
+
try:
|
|
412
|
+
report = build_traceability(args.design, args.manifest)
|
|
413
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
414
|
+
stdout.write(json.dumps({"decision": "block", "error": str(exc)}, ensure_ascii=False))
|
|
415
|
+
return 2
|
|
416
|
+
if args.json:
|
|
417
|
+
stdout.write(json.dumps(report, ensure_ascii=False))
|
|
418
|
+
else:
|
|
419
|
+
stdout.write(render_markdown(report))
|
|
420
|
+
return 0 if report["decision"] == "pass" else 3
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
if __name__ == "__main__":
|
|
424
|
+
raise SystemExit(main())
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def build_traceability_from_scenarios(scenarios_file: str, manifest_file: str) -> dict[str, object]:
|
|
428
|
+
"""Join the scenario source with manifest checks (0.9 source model)."""
|
|
429
|
+
from codeflow.acceptance.cf_scenario_source import parse_scenarios
|
|
430
|
+
source = parse_scenarios(scenarios_file)
|
|
431
|
+
manifest_scenarios, checks = _load_manifest(manifest_file)[1:]
|
|
432
|
+
source_ids = {str(item["id"]) for item in source["scenarios"]}
|
|
433
|
+
violations: list[dict[str, str]] = []
|
|
434
|
+
rows: list[dict[str, object]] = []
|
|
435
|
+
for item in source["scenarios"]:
|
|
436
|
+
scenario_id = str(item["id"])
|
|
437
|
+
row = manifest_scenarios.get(scenario_id)
|
|
438
|
+
required = [str(value) for value in item.get("checks", [])] or (
|
|
439
|
+
[str(row.get("check"))] if row is not None and isinstance(row.get("check"), str) else []
|
|
440
|
+
)
|
|
441
|
+
check_id = required[0] if required else None
|
|
442
|
+
check = checks.get(check_id) if check_id else None
|
|
443
|
+
missing_checks = [value for value in required if value not in checks]
|
|
444
|
+
exec_mode = str(item.get("exec") or "")
|
|
445
|
+
if row is None:
|
|
446
|
+
binding, status = "missing", "missing"
|
|
447
|
+
violations.append({"code": "missing_in_manifest", "scenario": scenario_id,
|
|
448
|
+
"message": f"{scenario_id} 在场景源中声明,但未进入 manifest"})
|
|
449
|
+
elif missing_checks:
|
|
450
|
+
binding, status = "unbound", str(row.get("status") or "planned")
|
|
451
|
+
violations.append({"code": "missing_check", "scenario": scenario_id,
|
|
452
|
+
"message": f"{scenario_id} 引用了不存在的 check: {', '.join(missing_checks)}"})
|
|
453
|
+
elif required and all(value in checks for value in required):
|
|
454
|
+
binding, status = "check", str(row.get("status") or "planned")
|
|
455
|
+
elif exec_mode == "aspirational":
|
|
456
|
+
binding, status = "aspirational", "aspirational"
|
|
457
|
+
else:
|
|
458
|
+
binding, status = "unbound", str(row.get("status") or "planned")
|
|
459
|
+
violations.append({"code": "unbound_scenario", "scenario": scenario_id,
|
|
460
|
+
"message": f"{scenario_id} 没有可执行 check,且未显式 aspirational"})
|
|
461
|
+
rows.append({
|
|
462
|
+
"id": scenario_id,
|
|
463
|
+
"feature": "",
|
|
464
|
+
"priority": str(item.get("priority") or ""),
|
|
465
|
+
"level": str(item.get("level") or ""),
|
|
466
|
+
"boundary": str(item.get("boundary") or ""),
|
|
467
|
+
"exec": exec_mode,
|
|
468
|
+
"env": str(check.get("env", "none")) if check else str(item.get("env") or "none"),
|
|
469
|
+
"inputs": list(check.get("inputs") or []) if check else [],
|
|
470
|
+
"check": check_id if isinstance(check_id, str) and check is not None else "-",
|
|
471
|
+
"command": list(check.get("command") or []) if check else [],
|
|
472
|
+
"owner": str(row.get("owner") or "") if row else "",
|
|
473
|
+
"status": status,
|
|
474
|
+
"binding": binding,
|
|
475
|
+
"in_design": False,
|
|
476
|
+
})
|
|
477
|
+
extras = _extra_rows(manifest_scenarios, checks, source_ids)
|
|
478
|
+
for row in extras:
|
|
479
|
+
violations.append({"code": "extra_in_manifest", "scenario": str(row["id"]),
|
|
480
|
+
"message": f"{row['id']} 存在于 manifest,但场景源未声明"})
|
|
481
|
+
if row["binding"] == "unbound":
|
|
482
|
+
violations.append({"code": "unbound_scenario", "scenario": str(row["id"]),
|
|
483
|
+
"message": f"{row['id']} 没有可执行 check,且未显式 aspirational"})
|
|
484
|
+
all_rows = rows + extras
|
|
485
|
+
summary = {
|
|
486
|
+
"design_scenarios": len(source["scenarios"]),
|
|
487
|
+
"manifest_scenarios": len(manifest_scenarios),
|
|
488
|
+
"bound": sum(1 for row in all_rows if row["binding"] == "check"),
|
|
489
|
+
"aspirational": sum(1 for row in all_rows if row["binding"] == "aspirational"),
|
|
490
|
+
"unbound": sum(1 for row in all_rows if row["binding"] == "unbound"),
|
|
491
|
+
"missing": sum(1 for row in all_rows if row["binding"] == "missing"),
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
"decision": "block" if violations else "pass",
|
|
495
|
+
"generated": True,
|
|
496
|
+
"source": str(scenarios_file),
|
|
497
|
+
"design_file": str(scenarios_file),
|
|
498
|
+
"manifest_file": str(manifest_file),
|
|
499
|
+
"summary": summary,
|
|
500
|
+
"scenarios": all_rows,
|
|
501
|
+
"violations": violations,
|
|
502
|
+
}
|
|
File without changes
|