@michaelxwb/code-flow 0.6.5 → 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.
Files changed (207) hide show
  1. package/README.md +47 -3
  2. package/docs/USAGE.md +1529 -0
  3. package/docs/acceptance-contract.md +217 -0
  4. package/docs/ci/acceptance-evidence.md +77 -0
  5. package/docs/migrations/runtime-0.7.0.md +41 -0
  6. package/docs/migrations/runtime-0.8.0.md +154 -0
  7. package/docs/migrations/runtime-0.9.0.md +40 -0
  8. package/docs/spec-injection-hardening.md +54 -0
  9. package/docs/spec-injection-performance.json +73 -0
  10. package/docs/spec-optimization.md +12 -0
  11. package/package.json +18 -3
  12. package/src/adapters/claude/CLAUDE.md +1 -1
  13. package/src/adapters/claude/commands/cf-init.md +1 -3
  14. package/src/adapters/claude/commands/cf-spec.md +23 -3
  15. package/src/adapters/claude/commands/cf-stats.md +11 -1
  16. package/src/adapters/claude/commands/cf-sync.md +20 -6
  17. package/src/adapters/claude/commands/cf-task/align.md +25 -3
  18. package/src/adapters/claude/commands/cf-task/archive.md +36 -42
  19. package/src/adapters/claude/commands/cf-task/block.md +15 -2
  20. package/src/adapters/claude/commands/cf-task/graph.md +13 -1
  21. package/src/adapters/claude/commands/cf-task/note.md +13 -1
  22. package/src/adapters/claude/commands/cf-task/plan.md +137 -23
  23. package/src/adapters/claude/commands/cf-task/prd.md +13 -2
  24. package/src/adapters/claude/commands/cf-task/start.md +52 -39
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
  27. package/src/adapters/claude/commands/cf-validate.md +30 -5
  28. package/src/adapters/codex/AGENTS.md +1 -1
  29. package/src/adapters/codex/hooks.json +14 -10
  30. package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
  31. package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
  32. package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
  33. package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
  34. package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
  35. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
  36. package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
  37. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
  38. package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
  39. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
  40. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
  41. package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
  42. package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
  43. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
  44. package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
  45. package/src/adapters/commands.json +115 -0
  46. package/src/adapters/costrict/CLAUDE.md +1 -1
  47. package/src/adapters/costrict/commands/cf-init.md +1 -3
  48. package/src/adapters/costrict/commands/cf-spec.md +23 -3
  49. package/src/adapters/costrict/commands/cf-stats.md +11 -1
  50. package/src/adapters/costrict/commands/cf-sync.md +20 -6
  51. package/src/adapters/costrict/commands/cf-task/align.md +25 -3
  52. package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
  53. package/src/adapters/costrict/commands/cf-task/block.md +15 -2
  54. package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
  55. package/src/adapters/costrict/commands/cf-task/note.md +13 -1
  56. package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
  57. package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
  58. package/src/adapters/costrict/commands/cf-task/start.md +52 -39
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
  61. package/src/adapters/costrict/commands/cf-validate.md +30 -5
  62. package/src/adapters/install-plan.json +100 -0
  63. package/src/adapters/opencode/AGENTS.md +1 -1
  64. package/src/adapters/opencode/commands/cf-init.md +7 -7
  65. package/src/adapters/opencode/commands/cf-learn.md +6 -6
  66. package/src/adapters/opencode/commands/cf-spec.md +34 -2
  67. package/src/adapters/opencode/commands/cf-stats.md +17 -7
  68. package/src/adapters/opencode/commands/cf-sync.md +21 -6
  69. package/src/adapters/opencode/commands/cf-task/align.md +25 -8
  70. package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
  71. package/src/adapters/opencode/commands/cf-task/block.md +16 -2
  72. package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
  73. package/src/adapters/opencode/commands/cf-task/note.md +14 -1
  74. package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
  75. package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
  76. package/src/adapters/opencode/commands/cf-task/start.md +53 -45
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
  79. package/src/adapters/opencode/commands/cf-validate.md +35 -9
  80. package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
  81. package/src/adapters/platform-bindings.json +370 -0
  82. package/src/cli.js +369 -316
  83. package/src/core/code-flow/config.yml +4 -1
  84. package/src/core/code-flow/runtime-commands.json +259 -0
  85. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
  86. package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
  87. package/src/core/code-flow/scripts/cf_audit.py +10 -0
  88. package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
  89. package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
  90. package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
  91. package/src/core/code-flow/scripts/cf_feedback.py +5 -51
  92. package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
  93. package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
  94. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
  95. package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
  96. package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
  98. package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
  99. package/src/core/code-flow/scripts/cf_stats.py +5 -501
  100. package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
  101. package/src/core/code-flow/scripts/cf_sync.py +5 -135
  102. package/src/core/code-flow/scripts/cf_task_index.py +5 -143
  103. package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
  105. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
  106. package/src/core/code-flow/scripts/cf_validation.py +6 -77
  107. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  108. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  109. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
  110. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
  111. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
  112. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
  113. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
  114. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
  115. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
  116. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
  117. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
  118. package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
  119. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
  120. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
  121. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
  122. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
  123. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  124. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
  125. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
  126. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
  127. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
  128. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
  129. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
  130. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
  131. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
  132. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
  133. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
  134. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
  135. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
  136. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
  137. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
  138. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
  139. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
  140. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  141. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
  142. package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
  143. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
  144. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
  145. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
  146. package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
  147. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
  148. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
  149. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
  150. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
  151. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
  152. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
  153. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
  154. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
  155. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  156. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
  157. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  158. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
  159. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
  160. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
  161. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
  162. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
  163. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
  164. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  165. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
  166. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
  167. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
  168. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
  169. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
  170. package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
  171. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
  172. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
  173. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
  174. package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
  175. package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
  176. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
  177. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
  178. package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
  179. package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
  180. package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
  181. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
  182. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
  183. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
  184. package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
  185. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  186. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
  187. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
  188. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
  189. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
  190. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
  191. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
  192. package/src/core/code-flow/scripts/runtime-resources.json +29 -0
  193. package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
  194. package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
  195. package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
  196. package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
  197. package/src/core/code-flow/validation.yml +9 -3
  198. package/src/migrate/runtime.js +213 -0
  199. package/src/migrate/spec-workflow.js +16 -1
  200. package/src/runtime.js +219 -0
  201. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  202. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  203. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  204. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  205. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  206. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  207. /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
@@ -1,506 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- import json
3
- import os
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_stats."""
4
3
  import sys
5
- from typing import Optional
6
-
7
- import cf_log
8
- from cf_checks import load_check_state
9
- from cf_core import (
10
- build_effective_mapping,
11
- build_spec_catalog,
12
- compress_content,
13
- estimate_tokens,
14
- load_config,
15
- project_instruction_file,
16
- non_injectable_specs,
17
- resolve_quality_loop,
18
- )
19
-
20
-
21
- def _violation_fixed(index: int, events: list) -> bool:
22
- """修正口径:违规后同会话同文件有后续编辑,且其后无同 check 再违规。
23
-
24
- 用日志追加顺序判先后(ts 仅秒级精度,同秒事件无法靠时间戳排序)。
25
- 保留单点查询语义;批量聚合请用 violation_fixed_batch(一次线性扫描)。
26
- """
27
- violation = events[index]
28
- v_data = violation.get("data") or {}
29
- later_edit = False
30
- for event in events[index + 1:]:
31
- if event.get("sid") != violation.get("sid"):
32
- continue
33
- data = event.get("data") or {}
34
- if event.get("event") == "edit" and data.get("file") == v_data.get("file"):
35
- later_edit = True
36
- if (
37
- later_edit
38
- and event.get("event") == "violation"
39
- and data.get("check_id") == v_data.get("check_id")
40
- and data.get("file") == v_data.get("file")
41
- ):
42
- return False
43
- return later_edit
44
-
45
-
46
- def violation_fixed_batch(events: list) -> list[bool]:
47
- """Batch twin of _violation_fixed: one reverse pass, identical semantics.
48
-
49
- Segments the timeline per (session, file) at edits: a violation is fixed
50
- iff an edit exists after it and no same-check violation exists in a later
51
- segment. No slicing, no per-violation rescan: O(n) time.
52
- """
53
- fixed: dict[int, bool] = {}
54
- edit_right: set[tuple[str, str]] = set()
55
- current: dict[tuple[str, str], set[str]] = {}
56
- later: dict[tuple[str, str], set[str]] = {}
57
- for index in range(len(events) - 1, -1, -1):
58
- event = events[index]
59
- if not isinstance(event, dict):
60
- continue
61
- kind = event.get("event")
62
- data = event.get("data")
63
- if not isinstance(data, dict):
64
- data = {}
65
- key = (str(event.get("sid")), str(data.get("file")))
66
- if kind == "edit":
67
- later.setdefault(key, set()).update(current.get(key, ()))
68
- current[key] = set()
69
- edit_right.add(key)
70
- elif kind == "violation":
71
- check = str(data.get("check_id"))
72
- fixed[index] = key in edit_right and check not in later.get(key, ())
73
- current.setdefault(key, set()).add(check)
74
- return [fixed[index] for index, event in enumerate(events) if isinstance(event, dict) and event.get("event") == "violation"]
75
-
76
-
77
- def quality_loop_summary(project_root: str, config: dict) -> dict:
78
- """FEAT-05/02 度量聚合:Top 违规榜 / 修正率 / 误报与停用 / 降级组件。"""
79
- switches = resolve_quality_loop(config)
80
- events = cf_log.read_events(project_root, days=30)
81
- summary = {"switches": switches, "window_days": 30}
82
- if not events:
83
- summary["note"] = "暂无数据"
84
- return summary
85
-
86
- violations = [e for e in events if e.get("event") == "violation"]
87
- counts: dict = {}
88
- for v in violations:
89
- data = v.get("data") or {}
90
- key = f"{data.get('spec', '?')}#{data.get('check_id', '?')}"
91
- counts[key] = counts.get(key, 0) + 1
92
- summary["top_violations"] = sorted(
93
- ({"rule": k, "count": n} for k, n in counts.items()),
94
- key=lambda item: -item["count"],
95
- )[:10]
96
-
97
- fixed = violation_fixed_batch(events)
98
- summary["violation_total"] = len(violations)
99
- summary["fix_rate"] = (
100
- f"{round(sum(fixed) * 100 / len(violations))}%" if violations else "n/a"
101
- )
102
-
103
- state = load_check_state(project_root)
104
- summary["checks"] = {
105
- cid: {
106
- "hit_count": entry.get("hit_count", 0),
107
- "fp_count": entry.get("fp_count", 0),
108
- "disabled": bool(entry.get("disabled")),
109
- "disabled_reason": entry.get("disabled_reason", ""),
110
- }
111
- for cid, entry in state.items()
112
- if isinstance(entry, dict) and not cid.startswith("_")
113
- }
114
-
115
- degraded: dict = {}
116
- for event in events:
117
- if event.get("event") != "degrade":
118
- continue
119
- data = event.get("data") or {}
120
- component = data.get("component", "?")
121
- item = degraded.setdefault(component, {"count": 0, "last_error": ""})
122
- item["count"] += 1
123
- item["last_error"] = data.get("error", "")
124
- summary["degraded"] = degraded
125
- return summary
126
-
127
-
128
- def _percent(numerator: int, denominator: int) -> str:
129
- return f"{round(numerator * 100 / denominator)}%" if denominator else "n/a"
130
-
131
-
132
- def spec_workflow_summary(project_root: str) -> dict:
133
- events = cf_log.read_events(project_root, days=30)
134
- candidates = {
135
- (str((event.get("data") or {}).get("task")), str((event.get("data") or {}).get("rule")))
136
- for event in events if event.get("event") == "spec_candidate"
137
- }
138
- bound = {
139
- (str((event.get("data") or {}).get("task")), str((event.get("data") or {}).get("rule")))
140
- for event in events if event.get("event") == "spec_bound"
141
- }
142
- gates = [event for event in events if event.get("event") == "spec_gate"]
143
- first_pass = sum((event.get("data") or {}).get("first_pass") is True for event in gates)
144
- late = sum(event.get("event") == "late_violation" for event in events)
145
- statuses = {"stale": 0, "conflict": 0, "not_applicable": 0}
146
- for event in events:
147
- status = (event.get("data") or {}).get("status")
148
- if status in statuses:
149
- statuses[status] += 1
150
- degraded = sum(event.get("event") == "metrics_degraded" for event in events)
151
- return {
152
- "window_days": 30,
153
- "coverage": _percent(len(bound & candidates), len(candidates)),
154
- "first_compliance_rate": _percent(first_pass, len(gates)),
155
- "late_violation_rate": _percent(late, len(bound)),
156
- "statuses": statuses,
157
- "data_complete": degraded == 0,
158
- "degraded_events": degraded,
159
- "read_note": "read 仅表示读取,不代表理解",
160
- }
161
-
162
-
163
- def read_text(path: str) -> str:
164
- try:
165
- with open(path, "r", encoding="utf-8") as file:
166
- return file.read().strip()
167
- except Exception:
168
- return ""
169
-
170
-
171
- def normalize_rel_path(path: str) -> str:
172
- return path.replace(os.sep, "/")
173
-
174
-
175
- def extract_spec_path(spec_entry) -> str:
176
- if isinstance(spec_entry, dict):
177
- return normalize_rel_path(spec_entry.get("path", ""))
178
- if isinstance(spec_entry, str):
179
- return normalize_rel_path(spec_entry)
180
- return ""
181
-
182
-
183
- def discover_specs(specs_root: str) -> dict:
184
- discovered = {}
185
- if not os.path.isdir(specs_root):
186
- return discovered
187
-
188
- for root, _, files in os.walk(specs_root):
189
- for filename in files:
190
- if not filename.endswith(".md"):
191
- continue
192
- full_path = os.path.join(root, filename)
193
- rel = normalize_rel_path(os.path.relpath(full_path, specs_root))
194
- parts = rel.split("/", 1)
195
- if len(parts) < 2:
196
- continue
197
- domain = parts[0]
198
- discovered.setdefault(domain, []).append(rel)
199
-
200
- for domain in discovered:
201
- discovered[domain] = sorted(set(discovered[domain]))
202
- return discovered
203
-
204
-
205
- def configured_specs(config: dict, domain: str) -> list:
206
- mapping = (config.get("path_mapping") or {}).get(domain) or {}
207
- specs_config = mapping.get("specs") or []
208
- result = []
209
- for spec_entry in specs_config:
210
- rel = extract_spec_path(spec_entry)
211
- if rel:
212
- result.append(rel)
213
- return result
214
-
215
-
216
- def resolve_domains(config: dict, discovered: dict, domain_filter: Optional[str]) -> list:
217
- if domain_filter:
218
- if domain_filter in discovered:
219
- return [domain_filter]
220
- if domain_filter in (config.get("path_mapping") or {}):
221
- return [domain_filter]
222
- return []
223
-
224
- if discovered:
225
- return sorted(discovered.keys())
226
- return sorted((config.get("path_mapping") or {}).keys())
227
-
228
-
229
- def _build_item(rel: str, raw_content: str) -> dict:
230
- raw_tokens = estimate_tokens(raw_content)
231
- compressed_tokens = estimate_tokens(compress_content(raw_content))
232
- saved_pct = (
233
- round((raw_tokens - compressed_tokens) * 100 / raw_tokens, 1)
234
- if raw_tokens
235
- else 0.0
236
- )
237
- return {
238
- "path": rel,
239
- "tokens": compressed_tokens,
240
- "tokens_raw": raw_tokens,
241
- "tokens_compressed": compressed_tokens,
242
- "saved_pct": saved_pct,
243
- }
244
-
245
-
246
- def collect_domain_items(
247
- specs_root: str,
248
- domain: str,
249
- configured: list,
250
- discovered: list,
251
- ) -> tuple:
252
- items = []
253
- missing = []
254
- seen = set()
255
-
256
- for rel in configured:
257
- if rel in seen:
258
- continue
259
- seen.add(rel)
260
- full_path = os.path.join(specs_root, rel)
261
- if not os.path.exists(full_path):
262
- missing.append({"domain": domain, "path": rel})
263
- continue
264
- content = read_text(full_path)
265
- if content:
266
- items.append(_build_item(rel, content))
267
-
268
- for rel in discovered:
269
- if rel in seen:
270
- continue
271
- seen.add(rel)
272
- full_path = os.path.join(specs_root, rel)
273
- content = read_text(full_path)
274
- if content:
275
- items.append(_build_item(rel, content))
276
-
277
- return items, missing
278
-
279
-
280
- def main() -> None:
281
- project_root = os.getcwd()
282
- config = load_config(project_root)
283
- budget_cfg = config.get("budget") or {}
284
-
285
- human_output = "--human" in sys.argv
286
- json_output = not human_output
287
- audit_mode = "--audit" in sys.argv
288
- domain_filter = None
289
- for arg in sys.argv[1:]:
290
- if arg.startswith("--domain="):
291
- domain_filter = arg.split("=", 1)[1]
292
-
293
- l0_budget = budget_cfg.get("l0_max", 800)
294
- l1_budget = budget_cfg.get("l1_max", 1700)
295
- total_budget = budget_cfg.get("total", l0_budget + l1_budget)
296
-
297
- try:
298
- l0_budget = int(l0_budget)
299
- except Exception:
300
- l0_budget = 800
301
- try:
302
- total_budget = int(total_budget)
303
- except Exception:
304
- total_budget = l0_budget + l1_budget
305
-
306
- platform = next((arg.split("=", 1)[1] for arg in sys.argv if arg.startswith("--platform=")), "")
307
- instruction_file = project_instruction_file(project_root, platform)
308
- claude_path = os.path.join(project_root, instruction_file)
309
- l0_tokens = 0
310
- if os.path.exists(claude_path):
311
- l0_tokens = estimate_tokens(read_text(claude_path))
312
-
313
- l1 = {}
314
- total_tokens = l0_tokens
315
- templates_tokens = 0
316
- specs_root = os.path.join(project_root, ".code-flow", "specs")
317
- spec_domain_map = {}
318
- missing_specs = []
319
- domains_with_no_loaded_specs = []
320
- discovered = discover_specs(specs_root)
321
- domains = resolve_domains(config, discovered, domain_filter)
322
- config_fallback_mode = not discovered
323
-
324
- effective_mapping = build_effective_mapping(
325
- project_root, config.get("path_mapping") or {}
326
- )
327
- excluded_specs = non_injectable_specs(effective_mapping)
328
-
329
- for domain in domains:
330
- configured = configured_specs(config, domain)
331
- discovered_paths = discovered.get(domain, [])
332
- items, missing = collect_domain_items(specs_root, domain, configured, discovered_paths)
333
-
334
- for rel in configured:
335
- if not rel:
336
- continue
337
- spec_domain_map[rel] = domain
338
- for rel in discovered_paths:
339
- spec_domain_map[rel] = domain
340
-
341
- missing_specs.extend(missing)
342
-
343
- for item in items:
344
- item["injectable"] = item["path"] not in excluded_specs
345
- if items:
346
- l1[domain] = items
347
- total_tokens += sum(i["tokens"] for i in items if i["injectable"])
348
- templates_tokens += sum(i["tokens"] for i in items if not i["injectable"])
349
- elif configured and (config_fallback_mode or discovered_paths):
350
- domains_with_no_loaded_specs.append(domain)
351
-
352
- utilization = "0%"
353
- if total_budget:
354
- utilization = f"{round(total_tokens * 100 / total_budget)}%"
355
-
356
- warnings = []
357
- if l0_tokens > l0_budget:
358
- warnings.append("L0 超出预算")
359
- l1_tokens = total_tokens - l0_tokens
360
- if l1_tokens > l1_budget:
361
- warnings.append("L1 超出预算")
362
- if total_tokens > total_budget:
363
- warnings.append("总预算超出")
364
- if missing_specs:
365
- warnings.append(f"配置的 spec 文件缺失: {len(missing_specs)} 个")
366
- if domains_with_no_loaded_specs:
367
- domains_text = ", ".join(sorted(set(domains_with_no_loaded_specs)))
368
- warnings.append(f"以下域未加载到任何 L1 spec: {domains_text}")
369
-
370
- total_raw = sum(
371
- item.get("tokens_raw", item["tokens"])
372
- for items in l1.values()
373
- for item in items
374
- if item.get("injectable", True)
375
- )
376
- total_compressed = sum(
377
- item.get("tokens_compressed", item["tokens"])
378
- for items in l1.values()
379
- for item in items
380
- if item.get("injectable", True)
381
- )
382
- total_saved_pct = (
383
- round((total_raw - total_compressed) * 100 / total_raw, 1)
384
- if total_raw
385
- else 0.0
386
- )
387
- compression_summary = {
388
- "total_raw": total_raw,
389
- "total_compressed": total_compressed,
390
- "total_saved_pct": total_saved_pct,
391
- }
392
-
393
- try:
394
- catalog_max = int(budget_cfg.get("catalog_max", 200))
395
- except (TypeError, ValueError):
396
- catalog_max = 200
397
- catalog_text = build_spec_catalog(project_root, effective_mapping, catalog_max)
398
- catalog_summary = {
399
- "mode": "context_first",
400
- "tokens": estimate_tokens(catalog_text),
401
- "budget": catalog_max,
402
- "entries": catalog_text.count("\n- `"),
403
- }
404
-
405
- ql_summary = quality_loop_summary(project_root, config)
406
- workflow_summary = spec_workflow_summary(project_root)
407
-
408
- output = {
409
- "l0": {"file": instruction_file, "tokens": l0_tokens, "budget": l0_budget},
410
- "l1": l1,
411
- "total_tokens": total_tokens,
412
- "total_budget": total_budget,
413
- "utilization": utilization,
414
- "warnings": warnings,
415
- "spec_domain_map": spec_domain_map,
416
- "missing_specs": missing_specs,
417
- "compression_summary": compression_summary,
418
- "catalog": catalog_summary,
419
- "quality_loop": ql_summary,
420
- "spec_workflow": workflow_summary,
421
- "templates": {
422
- "tokens": templates_tokens,
423
- "files": sorted(excluded_specs),
424
- "note": "tags:[] 命令专用模板,永不自动注入,不计预算",
425
- },
426
- }
427
- if audit_mode:
428
- from cf_scan import build_report
429
- scan = build_report(project_root, platform)
430
- output["audit"] = {
431
- "files": [e for e in scan["files"] if e.get("issues")],
432
- "review": scan["review"],
433
- }
434
-
435
- if json_output:
436
- print(json.dumps(output, ensure_ascii=False))
437
- return
438
-
439
- print(f"L0 ({instruction_file}):", f"{l0_tokens} / {l0_budget}")
440
- for domain, items in l1.items():
441
- total_domain = sum(i["tokens"] for i in items if i.get("injectable", True))
442
- print(f"L1 {domain}:", total_domain)
443
- for item in items:
444
- raw = item.get("tokens_raw", item["tokens"])
445
- compressed = item.get("tokens_compressed", item["tokens"])
446
- saved = item.get("saved_pct", 0.0)
447
- suffix = "" if item.get("injectable", True) else "(模板,不计预算)"
448
- print(" -", item["path"], item["tokens"],
449
- f"(raw={raw}→compressed={compressed}, -{saved}%)" + suffix)
450
- if templates_tokens:
451
- print("TEMPLATES (非注入):", f"{templates_tokens} tokens,不计预算")
452
- if missing_specs:
453
- print("MISSING SPECS:")
454
- for item in missing_specs:
455
- print(" -", item["domain"], item["path"])
456
- print("TOTAL:", f"{total_tokens} / {total_budget}")
457
- print("UTILIZATION:", utilization)
458
- print(
459
- "COMPRESSION:",
460
- f"{total_raw} → {total_compressed} (-{total_saved_pct}%)",
461
- )
462
- print(
463
- "CATALOG:",
464
- "mode=context_first,",
465
- f"{catalog_summary['tokens']} / {catalog_max} tokens,",
466
- f"{catalog_summary['entries']} entries",
467
- )
468
- switches = ql_summary["switches"]
469
- print("QUALITY-LOOP:", "enabled" if switches["enabled"] else "disabled")
470
- if ql_summary.get("note"):
471
- print(" -", ql_summary["note"])
472
- else:
473
- print(
474
- " - violations:", ql_summary.get("violation_total", 0),
475
- "| fix_rate:", ql_summary.get("fix_rate", "n/a"),
476
- )
477
- for item in ql_summary.get("top_violations", [])[:5]:
478
- print(" ·", item["rule"], item["count"])
479
- for cid, info in ql_summary.get("checks", {}).items():
480
- if info["disabled"] or info["fp_count"]:
481
- print(
482
- f" · {cid}: hits={info['hit_count']} fp={info['fp_count']}"
483
- + (f" DISABLED({info['disabled_reason']})" if info["disabled"] else "")
484
- )
485
- for component, info in ql_summary.get("degraded", {}).items():
486
- print(f" · degraded {component}: {info['count']} ({info['last_error']})")
487
- if warnings:
488
- print("WARNINGS:", "; ".join(warnings))
489
-
490
- if audit_mode:
491
- audit = output["audit"]
492
- print("AUDIT (规范质量):")
493
- if not audit["files"] and not audit["review"]:
494
- print(" - 无问题")
495
- for entry in audit["files"]:
496
- print(" -", entry["path"], "|", " / ".join(entry["issues"]))
497
- if audit["review"]:
498
- print(" REVIEW (待复审):")
499
- for item in audit["review"]:
500
- print(f" · {item['item']}: {item['reason']}")
501
- else:
502
- print("AUDIT: 运行 cf-stats --audit 查看规范质量问题与待复审清单")
503
4
 
5
+ from codeflow.cli import cf_stats as _impl
504
6
 
505
7
  if __name__ == "__main__":
506
- main()
8
+ _impl.main()
9
+ else:
10
+ sys.modules[__name__] = _impl