@michaelxwb/code-flow 0.6.5 → 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 (177) hide show
  1. package/README.md +75 -110
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +268 -0
  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 +21 -9
  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 +22 -19
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
  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/migrations/spec-workflow-0.6.0.md +0 -42
  54. package/src/adapters/claude/CLAUDE.md +0 -45
  55. package/src/adapters/claude/commands/cf-init.md +0 -264
  56. package/src/adapters/claude/commands/cf-learn.md +0 -291
  57. package/src/adapters/claude/commands/cf-spec.md +0 -67
  58. package/src/adapters/claude/commands/cf-stats.md +0 -86
  59. package/src/adapters/claude/commands/cf-sync.md +0 -25
  60. package/src/adapters/claude/commands/cf-task/align.md +0 -309
  61. package/src/adapters/claude/commands/cf-task/archive.md +0 -161
  62. package/src/adapters/claude/commands/cf-task/block.md +0 -36
  63. package/src/adapters/claude/commands/cf-task/graph.md +0 -80
  64. package/src/adapters/claude/commands/cf-task/note.md +0 -236
  65. package/src/adapters/claude/commands/cf-task/plan.md +0 -363
  66. package/src/adapters/claude/commands/cf-task/prd.md +0 -162
  67. package/src/adapters/claude/commands/cf-task/start.md +0 -304
  68. package/src/adapters/claude/commands/cf-task/status.md +0 -59
  69. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
  70. package/src/adapters/claude/commands/cf-validate.md +0 -75
  71. package/src/adapters/claude/settings.local.json +0 -50
  72. package/src/adapters/codex/AGENTS.md +0 -45
  73. package/src/adapters/codex/config.toml +0 -3
  74. package/src/adapters/codex/hooks.json +0 -50
  75. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  76. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  77. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
  78. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
  79. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
  80. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
  81. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
  82. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
  83. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
  84. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
  85. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
  86. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
  87. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
  88. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
  89. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
  90. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
  91. package/src/adapters/costrict/CLAUDE.md +0 -45
  92. package/src/adapters/costrict/commands/cf-init.md +0 -264
  93. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  94. package/src/adapters/costrict/commands/cf-spec.md +0 -67
  95. package/src/adapters/costrict/commands/cf-stats.md +0 -86
  96. package/src/adapters/costrict/commands/cf-sync.md +0 -25
  97. package/src/adapters/costrict/commands/cf-task/align.md +0 -309
  98. package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
  99. package/src/adapters/costrict/commands/cf-task/block.md +0 -36
  100. package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
  101. package/src/adapters/costrict/commands/cf-task/note.md +0 -236
  102. package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
  103. package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
  104. package/src/adapters/costrict/commands/cf-task/start.md +0 -304
  105. package/src/adapters/costrict/commands/cf-task/status.md +0 -59
  106. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
  107. package/src/adapters/costrict/commands/cf-validate.md +0 -75
  108. package/src/adapters/costrict/settings.local.json +0 -50
  109. package/src/adapters/opencode/AGENTS.md +0 -45
  110. package/src/adapters/opencode/commands/cf-init.md +0 -281
  111. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  112. package/src/adapters/opencode/commands/cf-spec.md +0 -59
  113. package/src/adapters/opencode/commands/cf-stats.md +0 -90
  114. package/src/adapters/opencode/commands/cf-sync.md +0 -29
  115. package/src/adapters/opencode/commands/cf-task/align.md +0 -314
  116. package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
  117. package/src/adapters/opencode/commands/cf-task/block.md +0 -40
  118. package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
  119. package/src/adapters/opencode/commands/cf-task/note.md +0 -240
  120. package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
  121. package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
  122. package/src/adapters/opencode/commands/cf-task/start.md +0 -309
  123. package/src/adapters/opencode/commands/cf-task/status.md +0 -63
  124. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
  125. package/src/adapters/opencode/commands/cf-validate.md +0 -79
  126. package/src/adapters/opencode/opencode.json +0 -3
  127. package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
  128. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  129. package/src/cli.js +0 -1023
  130. package/src/core/code-flow/config.yml +0 -158
  131. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  132. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
  133. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
  134. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  135. package/src/core/code-flow/scripts/cf_checks.py +0 -336
  136. package/src/core/code-flow/scripts/cf_core.py +0 -787
  137. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  138. package/src/core/code-flow/scripts/cf_feedback.py +0 -56
  139. package/src/core/code-flow/scripts/cf_log.py +0 -154
  140. package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
  141. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
  142. package/src/core/code-flow/scripts/cf_scan.py +0 -295
  143. package/src/core/code-flow/scripts/cf_session_state.py +0 -37
  144. package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
  145. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
  146. package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
  147. package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
  148. package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
  149. package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
  150. package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
  151. package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
  152. package/src/core/code-flow/scripts/cf_stats.py +0 -506
  153. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
  154. package/src/core/code-flow/scripts/cf_sync.py +0 -140
  155. package/src/core/code-flow/scripts/cf_task_index.py +0 -148
  156. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
  157. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  158. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  159. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
  160. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
  161. package/src/core/code-flow/scripts/cf_validation.py +0 -81
  162. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  163. package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
  164. package/src/core/code-flow/specs/backend/_map.md +0 -56
  165. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  166. package/src/core/code-flow/specs/backend/database.md +0 -51
  167. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  168. package/src/core/code-flow/specs/backend/logging.md +0 -50
  169. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  170. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  171. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  172. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  173. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  174. package/src/core/code-flow/specs/shared/_map.md +0 -57
  175. package/src/core/code-flow/tasks/.gitkeep +0 -0
  176. package/src/core/code-flow/validation.yml +0 -53
  177. package/src/migrate/spec-workflow.js +0 -238
@@ -1,506 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import os
4
- 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
-
504
-
505
- if __name__ == "__main__":
506
- main()