@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.
Files changed (268) hide show
  1. package/README.md +73 -152
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +156 -1417
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +18 -21
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +12 -12
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/acceptance-contract.md +0 -217
  54. package/docs/ci/acceptance-evidence.md +0 -77
  55. package/docs/migrations/runtime-0.7.0.md +0 -41
  56. package/docs/migrations/runtime-0.8.0.md +0 -154
  57. package/docs/migrations/runtime-0.9.0.md +0 -40
  58. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  59. package/docs/spec-injection-hardening.md +0 -54
  60. package/docs/spec-injection-performance.json +0 -73
  61. package/docs/spec-optimization.md +0 -12
  62. package/src/adapters/claude/CLAUDE.md +0 -45
  63. package/src/adapters/claude/commands/cf-init.md +0 -262
  64. package/src/adapters/claude/commands/cf-learn.md +0 -291
  65. package/src/adapters/claude/commands/cf-spec.md +0 -87
  66. package/src/adapters/claude/commands/cf-stats.md +0 -96
  67. package/src/adapters/claude/commands/cf-sync.md +0 -39
  68. package/src/adapters/claude/commands/cf-task/align.md +0 -331
  69. package/src/adapters/claude/commands/cf-task/archive.md +0 -155
  70. package/src/adapters/claude/commands/cf-task/block.md +0 -49
  71. package/src/adapters/claude/commands/cf-task/graph.md +0 -92
  72. package/src/adapters/claude/commands/cf-task/note.md +0 -248
  73. package/src/adapters/claude/commands/cf-task/plan.md +0 -477
  74. package/src/adapters/claude/commands/cf-task/prd.md +0 -173
  75. package/src/adapters/claude/commands/cf-task/start.md +0 -317
  76. package/src/adapters/claude/commands/cf-task/status.md +0 -71
  77. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
  78. package/src/adapters/claude/commands/cf-validate.md +0 -100
  79. package/src/adapters/claude/settings.local.json +0 -50
  80. package/src/adapters/codex/AGENTS.md +0 -45
  81. package/src/adapters/codex/config.toml +0 -3
  82. package/src/adapters/codex/hooks.json +0 -54
  83. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  84. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  85. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
  86. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
  87. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
  88. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
  89. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
  90. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
  91. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
  92. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
  93. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
  94. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
  95. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
  96. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
  97. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
  98. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
  99. package/src/adapters/commands.json +0 -115
  100. package/src/adapters/costrict/CLAUDE.md +0 -45
  101. package/src/adapters/costrict/commands/cf-init.md +0 -262
  102. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  103. package/src/adapters/costrict/commands/cf-spec.md +0 -87
  104. package/src/adapters/costrict/commands/cf-stats.md +0 -96
  105. package/src/adapters/costrict/commands/cf-sync.md +0 -39
  106. package/src/adapters/costrict/commands/cf-task/align.md +0 -331
  107. package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
  108. package/src/adapters/costrict/commands/cf-task/block.md +0 -49
  109. package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
  110. package/src/adapters/costrict/commands/cf-task/note.md +0 -248
  111. package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
  112. package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
  113. package/src/adapters/costrict/commands/cf-task/start.md +0 -317
  114. package/src/adapters/costrict/commands/cf-task/status.md +0 -71
  115. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
  116. package/src/adapters/costrict/commands/cf-validate.md +0 -100
  117. package/src/adapters/costrict/settings.local.json +0 -50
  118. package/src/adapters/install-plan.json +0 -100
  119. package/src/adapters/opencode/AGENTS.md +0 -45
  120. package/src/adapters/opencode/commands/cf-init.md +0 -281
  121. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  122. package/src/adapters/opencode/commands/cf-spec.md +0 -91
  123. package/src/adapters/opencode/commands/cf-stats.md +0 -100
  124. package/src/adapters/opencode/commands/cf-sync.md +0 -44
  125. package/src/adapters/opencode/commands/cf-task/align.md +0 -331
  126. package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
  127. package/src/adapters/opencode/commands/cf-task/block.md +0 -54
  128. package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
  129. package/src/adapters/opencode/commands/cf-task/note.md +0 -253
  130. package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
  131. package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
  132. package/src/adapters/opencode/commands/cf-task/start.md +0 -317
  133. package/src/adapters/opencode/commands/cf-task/status.md +0 -76
  134. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
  135. package/src/adapters/opencode/commands/cf-validate.md +0 -105
  136. package/src/adapters/opencode/opencode.json +0 -3
  137. package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
  138. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  139. package/src/adapters/platform-bindings.json +0 -370
  140. package/src/cli.js +0 -1076
  141. package/src/core/code-flow/config.yml +0 -161
  142. package/src/core/code-flow/runtime-commands.json +0 -259
  143. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
  144. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
  145. package/src/core/code-flow/scripts/cf_audit.py +0 -10
  146. package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
  147. package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
  148. package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
  149. package/src/core/code-flow/scripts/cf_feedback.py +0 -10
  150. package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
  151. package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
  152. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
  153. package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
  154. package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
  155. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
  156. package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
  157. package/src/core/code-flow/scripts/cf_stats.py +0 -10
  158. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
  159. package/src/core/code-flow/scripts/cf_sync.py +0 -10
  160. package/src/core/code-flow/scripts/cf_task_index.py +0 -10
  161. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
  162. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
  163. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
  164. package/src/core/code-flow/scripts/cf_validation.py +0 -10
  165. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  166. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  167. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +0 -347
  168. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
  169. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
  170. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
  171. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
  172. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
  173. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
  174. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
  175. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
  176. package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
  177. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
  178. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
  179. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
  180. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
  181. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  182. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
  183. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
  184. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
  185. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
  186. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
  187. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
  188. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
  189. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
  190. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
  191. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
  192. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
  193. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
  194. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
  195. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
  196. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
  197. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
  198. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  199. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
  200. package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
  201. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
  202. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
  203. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
  204. package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
  205. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
  206. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
  207. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
  208. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
  209. package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
  210. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
  211. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
  212. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
  213. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
  214. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  215. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
  216. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  217. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
  218. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
  219. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
  220. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
  221. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
  222. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
  223. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  224. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
  225. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
  226. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
  227. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
  228. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
  229. package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
  230. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
  231. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
  232. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
  233. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
  234. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
  235. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
  236. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
  237. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
  238. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
  239. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
  240. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
  241. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
  242. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
  243. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
  244. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  245. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
  246. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
  247. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
  248. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
  249. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
  250. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
  251. package/src/core/code-flow/scripts/runtime-resources.json +0 -29
  252. package/src/core/code-flow/specs/backend/_map.md +0 -56
  253. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  254. package/src/core/code-flow/specs/backend/database.md +0 -51
  255. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  256. package/src/core/code-flow/specs/backend/logging.md +0 -50
  257. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  258. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  259. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  260. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  261. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  262. package/src/core/code-flow/specs/shared/_map.md +0 -57
  263. package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
  264. package/src/core/code-flow/tasks/.gitkeep +0 -0
  265. package/src/core/code-flow/validation.yml +0 -59
  266. package/src/migrate/runtime.js +0 -213
  267. package/src/migrate/spec-workflow.js +0 -253
  268. package/src/runtime.js +0 -219
@@ -1,533 +0,0 @@
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