@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,367 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import glob
4
- import os
5
- import re
6
- import sys
7
- from pathlib import Path
8
-
9
- from codeflow.core import cf_log
10
- from codeflow.acceptance.cf_checks import load_check_state, parse_spec_checks
11
- from codeflow.core.cf_core import (
12
- build_effective_mapping,
13
- build_spec_catalog,
14
- estimate_tokens,
15
- load_config,
16
- project_instruction_file,
17
- non_injectable_specs,
18
- parse_spec_frontmatter,
19
- )
20
-
21
- REVIEW_MIN_COVERAGE_DAYS = 7
22
-
23
-
24
- def _log_coverage_sufficient(events: list) -> bool:
25
- """"未命中"信号需要足够的日志覆盖期——刚部署一天的日志没资格断言
26
- "30 天未命中"(实测误伤:单条事件就把全部 spec 标为待复审)。"""
27
- from datetime import datetime, timedelta
28
- earliest = min((str(e.get("ts", "")) for e in events), default="")
29
- try:
30
- first = datetime.fromisoformat(earliest)
31
- except ValueError:
32
- return False
33
- return first <= datetime.now() - timedelta(days=REVIEW_MIN_COVERAGE_DAYS)
34
-
35
-
36
- def build_review_list(project_root: str, specs: list) -> list:
37
- """待复审清单(FEAT-06):未命中注入 / 已停用 / 反复误报。
38
-
39
- 豁免:check-state `_review_exempt` 列表中的条目不再提示(S-09 处置闭环)。
40
- 日志覆盖不足 REVIEW_MIN_COVERAGE_DAYS 天时跳过"未命中"信号,
41
- 避免误伤全新安装与刚部署项目。
42
- """
43
- state = load_check_state(project_root)
44
- exempt = set(state.get("_review_exempt") or [])
45
- review: list = []
46
-
47
- events = cf_log.read_events(project_root, days=30)
48
- if events and _log_coverage_sufficient(events):
49
- engaged: set = set()
50
- for event in events:
51
- data = event.get("data") or {}
52
- if event.get("event") == "inject":
53
- engaged.update(data.get("specs") or [])
54
- elif event.get("event") == "violation" and data.get("spec"):
55
- engaged.add(data["spec"])
56
- for spec in specs:
57
- rel = spec.get("rel", "")
58
- if rel.endswith("_map.md") or rel.startswith("shared/"):
59
- continue
60
- if rel not in engaged and rel not in exempt:
61
- review.append({"item": rel, "reason": "30 天未命中注入/未触发检查"})
62
-
63
- for cid, entry in state.items():
64
- if cid.startswith("_") or not isinstance(entry, dict) or cid in exempt:
65
- continue
66
- if entry.get("disabled"):
67
- review.append({
68
- "item": cid,
69
- "reason": f"已自动停用({entry.get('disabled_reason', '')}),待改写或删除",
70
- })
71
- elif int(entry.get("fp_count", 0)) >= 2:
72
- review.append({"item": cid, "reason": f"误报 {entry['fp_count']} 次"})
73
- return review
74
-
75
-
76
- PATH_PATTERN = re.compile(r"(?:[\w./-]+/)+[\w.-]+\.[A-Za-z0-9]+")
77
-
78
-
79
- def read_text(path: str) -> str:
80
- try:
81
- with open(path, "r", encoding="utf-8") as file:
82
- return file.read().strip()
83
- except Exception:
84
- return ""
85
-
86
-
87
- def normalize_line(line: str) -> str:
88
- return " ".join(line.strip().split())
89
-
90
-
91
- _YAML_STRUCTURE = re.compile(
92
- r"^(?:stages:.*|enforcement:.*|verifiers:.*|tags:.*|tier:.*|"
93
- r"- (?:design|plan|code|review)|-?\s*type:\s*(?:command|test))$"
94
- )
95
-
96
-
97
- def _audit_body(content: str) -> str:
98
- """Remove only a complete frontmatter block at the start of the file."""
99
- lines = content.splitlines()
100
- if lines and lines[0].strip() == "---":
101
- end = next((i for i in range(1, len(lines)) if lines[i].strip() == "---"), None)
102
- if end is not None:
103
- return "\n".join(lines[end + 1:])
104
- return content
105
-
106
-
107
- def find_redundant_lines(specs: list) -> dict:
108
- line_map: dict = {}
109
- for spec in specs:
110
- for raw in _audit_body(spec["content"]).splitlines():
111
- line = normalize_line(raw)
112
- if not line:
113
- continue
114
- if line.startswith("#") or line == "---" or _YAML_STRUCTURE.fullmatch(line):
115
- continue
116
- if len(line) < 6:
117
- continue
118
- line_map.setdefault(line, set()).add(spec["path"])
119
- redundant = {line: paths for line, paths in line_map.items() if len(paths) >= 3}
120
- return redundant
121
-
122
-
123
- def _audit_options(config: dict) -> tuple[list[str], int]:
124
- audit = config.get("audit", {})
125
- if not isinstance(audit, dict):
126
- raise ValueError("audit must be a mapping")
127
- roots = audit.get("path_roots", [])
128
- if not isinstance(roots, list) or any(not isinstance(root, str) or not root for root in roots):
129
- raise ValueError("audit.path_roots must be a list of paths")
130
- maximum = audit.get("long_spec_tokens", 500)
131
- if type(maximum) is not int or maximum <= 0:
132
- raise ValueError("audit.long_spec_tokens must be a positive integer")
133
- return roots, maximum
134
-
135
-
136
- def _audit_roots(project_root: str, configured: list[str]) -> tuple[list[str], list[dict[str, str]]]:
137
- project = Path(project_root).resolve()
138
- roots: list[str] = []
139
- warnings: list[dict[str, str]] = []
140
- discovered = [path for pattern in ("apps/*/src/*", "packages/*/src/*", "lib/*/src/*")
141
- for path in sorted(glob.glob(str(project / pattern)))]
142
- for value in [*configured, *discovered]:
143
- candidate = (project / value).resolve()
144
- if (candidate != project and project not in candidate.parents) or not candidate.is_dir() or not os.access(candidate, os.R_OK | os.X_OK):
145
- if value in configured:
146
- warnings.append({"code": "invalid_path_root", "path": value})
147
- continue
148
- resolved = str(candidate)
149
- if resolved not in roots:
150
- roots.append(resolved)
151
- return roots, warnings
152
-
153
-
154
- def _path_resolution(text: str, project_root: str, base_dir: str,
155
- roots: list[str]) -> tuple[list[str], dict[str, str]]:
156
- missing: list[str] = []
157
- resolved: dict[str, str] = {}
158
- project = Path(project_root).resolve()
159
- for token in _path_tokens(text):
160
- candidates = [str(project), base_dir, *roots]
161
- if os.path.isabs(token):
162
- candidates = [""]
163
- for root in candidates:
164
- if root and not Path(root).is_dir():
165
- continue
166
- target = Path(os.path.join(root, token)).resolve()
167
- if not os.path.isabs(token) and target != project and project not in target.parents:
168
- continue
169
- if target.exists():
170
- resolved[token] = os.path.relpath(root, project) if root else "/"
171
- break
172
- else:
173
- missing.append(token)
174
- return missing, resolved
175
-
176
-
177
- def _path_tokens(text: str) -> list[str]:
178
- tokens: set[str] = set()
179
- for word in re.findall(r"[^\s`<>()\"']+", text):
180
- if "://" in word or any(char in word for char in "*?{}[]"):
181
- continue
182
- tokens.update(PATH_PATTERN.findall(word))
183
- return sorted(tokens)
184
-
185
-
186
- def find_missing_paths(text: str, project_root: str, base_dir: str = "") -> list:
187
- """路径引用检查:项目根解析失败时,再按 spec 自身目录二次解析——
188
- _map.md 常用相对本域目录的引用(如 design/design-lite.md)。"""
189
- return _path_resolution(text, project_root, base_dir, [])[0]
190
-
191
-
192
- def build_report(project_root: str, platform: str = "") -> dict:
193
- """规范质量审计数据(cf-scan 引擎,亦供 cf-stats --audit 复用)。
194
-
195
- files 含 issues / template 标记,review 为待复审清单;展示层负责过滤与渲染。"""
196
- files = []
197
- total_tokens = 0
198
-
199
- instruction_file = project_instruction_file(project_root, platform)
200
- claude_path = os.path.join(project_root, instruction_file)
201
- if os.path.exists(claude_path):
202
- content = read_text(claude_path)
203
- tokens = estimate_tokens(content)
204
- total_tokens += tokens
205
- files.append({"path": instruction_file, "tokens": tokens, "issues": []})
206
-
207
- specs_root = os.path.join(project_root, ".code-flow", "specs")
208
- specs = []
209
- if os.path.isdir(specs_root):
210
- for root, _, filenames in os.walk(specs_root):
211
- for name in filenames:
212
- if not name.endswith(".md"):
213
- continue
214
- full_path = os.path.join(root, name)
215
- rel = os.path.relpath(full_path, specs_root)
216
- if rel.replace(os.sep, "/").startswith("_session/"):
217
- continue
218
- content = read_text(full_path)
219
- if not content:
220
- continue
221
- tokens = estimate_tokens(content)
222
- total_tokens += tokens
223
- rel_path = os.path.join("specs", rel).replace(os.sep, "/")
224
- spec_entry = {
225
- "path": rel_path,
226
- "rel": rel.replace(os.sep, "/"),
227
- "tokens": tokens,
228
- "issues": [],
229
- "content": content,
230
- }
231
- specs.append(spec_entry)
232
-
233
- # 模板标记前置:模板(tags:[])不参与冗长/冗余审计,也不把
234
- # 模板间共享的文档头计入其他文件的冗余计数
235
- config = load_config(project_root)
236
- configured_roots, long_spec_tokens = _audit_options(config)
237
- roots, audit_warnings = _audit_roots(project_root, configured_roots)
238
- effective_mapping = build_effective_mapping(
239
- project_root, config.get("path_mapping") or {}
240
- )
241
- excluded_specs = non_injectable_specs(effective_mapping)
242
- for spec in specs:
243
- spec["template"] = spec.get("rel", "") in excluded_specs
244
-
245
- redundant_map = find_redundant_lines([s for s in specs if not s["template"]])
246
-
247
- for spec in specs:
248
- issues = []
249
- if not spec["template"] and spec["tokens"] > long_spec_tokens:
250
- issues.append(f"冗长: {spec['tokens']} tokens")
251
-
252
- spec_dir = os.path.join(specs_root, os.path.dirname(spec.get("rel", "")))
253
- missing_paths, resolved_paths = _path_resolution(spec["content"], project_root, spec_dir, roots)
254
- for path in missing_paths[:3]:
255
- issues.append(f"过时: 路径不存在 {path}")
256
-
257
- redundant_lines = [line for line, paths in redundant_map.items() if spec["path"] in paths]
258
- for line in redundant_lines[:3]:
259
- issues.append(f"冗余: '{line}' 出现于 {len(redundant_map[line])} 个文件")
260
-
261
- # Constraint specs feed the catalog one line each — flag missing
262
- # frontmatter description (catalog falls back to blockquote/H1).
263
- rel = spec.get("rel", "")
264
- is_constraint = not rel.endswith("_map.md") and not rel.startswith("shared/")
265
- if is_constraint:
266
- meta, _ = parse_spec_frontmatter(spec["content"])
267
- if not (meta.get("description") or "").strip():
268
- issues.append("缺描述: 建议加 frontmatter description(catalog 适用场景一句话)")
269
- # checks 标注校验(E-01 / B-04 的离线出口)
270
- _, check_errors = parse_spec_checks(spec["content"])
271
- for error in check_errors[:3]:
272
- issues.append(f"checks: {error}")
273
-
274
- files.append({
275
- "path": spec["path"],
276
- "tokens": spec["tokens"],
277
- "issues": issues,
278
- "template": spec["template"],
279
- "resolved_paths": resolved_paths,
280
- })
281
-
282
- for entry in files:
283
- if total_tokens <= 0:
284
- entry["percent"] = "0%"
285
- else:
286
- entry["percent"] = f"{round(entry['tokens'] * 100 / total_tokens)}%"
287
-
288
- budget = (config.get("budget") or {}).get("total", 2500)
289
- try:
290
- budget = int(budget)
291
- except Exception:
292
- budget = 2500
293
-
294
- try:
295
- catalog_max = int((config.get("budget") or {}).get("catalog_max", 200))
296
- except Exception:
297
- catalog_max = 200
298
- catalog = build_spec_catalog(project_root, effective_mapping, catalog_max)
299
- catalog_info = {
300
- "tokens": estimate_tokens(catalog),
301
- "max": catalog_max,
302
- "entries": catalog.count("\n- `"),
303
- }
304
-
305
- review = build_review_list(project_root, specs)
306
-
307
- # 预算口径与 cf-stats 一致:tags:[] 命令模板不计入注入预算
308
- templates_tokens = sum(e["tokens"] for e in files if e.get("template"))
309
- injectable_tokens = total_tokens - templates_tokens
310
-
311
- return {
312
- "files": files,
313
- "total_tokens": injectable_tokens,
314
- "templates_tokens": templates_tokens,
315
- "budget": budget,
316
- "catalog": catalog_info,
317
- "review": review,
318
- "warnings": [],
319
- "audit_warnings": audit_warnings,
320
- }
321
-
322
-
323
- def main() -> None:
324
- json_output = "--json" in sys.argv
325
- only_issues = "--only-issues" in sys.argv
326
- limit = None
327
- for arg in sys.argv[1:]:
328
- if arg.startswith("--limit="):
329
- try:
330
- limit = int(arg.split("=", 1)[1])
331
- except Exception:
332
- limit = None
333
-
334
- report = build_report(os.getcwd())
335
- files = report["files"]
336
- if limit is not None:
337
- files = files[:limit]
338
- if only_issues:
339
- files = [entry for entry in files if entry.get("issues")]
340
-
341
- if json_output:
342
- print(json.dumps(dict(report, files=files), ensure_ascii=False))
343
- return
344
-
345
- catalog_info = report["catalog"]
346
- print("FILE | TOKENS | PERCENT | ISSUES")
347
- for entry in files:
348
- issues = entry.get("issues") or []
349
- issue_text = " / ".join(issues) if issues else "-"
350
- marker = " [模板]" if entry.get("template") else ""
351
- print(f"{entry['path']}{marker} | {entry['tokens']} | {entry['percent']} | {issue_text}")
352
- print("TOTAL:", f"{report['total_tokens']} / {report['budget']}")
353
- if report["templates_tokens"]:
354
- print("TEMPLATES (非注入):", f"{report['templates_tokens']} tokens,不计预算")
355
- print(
356
- "CATALOG:",
357
- f"{catalog_info['tokens']} / {catalog_info['max']} tokens,",
358
- f"{catalog_info['entries']} entries",
359
- )
360
- if report["review"]:
361
- print("REVIEW (待复审):")
362
- for item in report["review"]:
363
- print(f" - {item['item']}: {item['reason']}")
364
-
365
-
366
- if __name__ == "__main__":
367
- main()
@@ -1,55 +0,0 @@
1
- """Shared path-projection inputs and budget accounting."""
2
- from __future__ import annotations
3
-
4
- from pathlib import Path
5
- from typing import Sequence
6
-
7
- from codeflow.core.cf_core import assemble_context, compress_content, estimate_tokens, load_config, parse_spec_frontmatter, resolve_compress
8
- from codeflow.spec.cf_spec_resolver import resolve_candidates
9
-
10
-
11
- class SpecBudgetError(ValueError):
12
- def __init__(self, code: str, message: str) -> None:
13
- self.code = code
14
- super().__init__(message)
15
-
16
-
17
- def path_specs(root: str, paths: Sequence[str], compress: bool = True,
18
- matched_only: bool = True) -> list[dict[str, object]]:
19
- try:
20
- candidates = resolve_candidates(root, 'code', tuple(paths), matched_only=matched_only)
21
- except ValueError as exc:
22
- raise SpecBudgetError('invalid_spec_metadata', str(exc)) from exc
23
- specs: list[dict[str, object]] = []
24
- for candidate in candidates:
25
- if matched_only and candidate.scope == 'unmatched':
26
- continue
27
- target = Path(root) / '.code-flow/specs' / candidate.path
28
- try:
29
- raw = target.read_text(encoding='utf-8')
30
- except OSError as exc:
31
- raise SpecBudgetError('spec_read_failed', f'{candidate.path}: {exc}') from exc
32
- _, content = parse_spec_frontmatter(raw)
33
- final = compress_content(content.strip()) if compress else content.strip()
34
- specs.append({'path': candidate.path, 'content': final, 'tier': 1})
35
- return specs
36
-
37
-
38
- def measure_path_budget(root: str, paths: Sequence[str]) -> dict[str, object]:
39
- config = load_config(root)
40
- quality = config.get('quality_loop')
41
- compress = resolve_compress(quality if isinstance(quality, dict) else {})
42
- matched = path_specs(root, paths, compress) if paths else []
43
- aggregate = path_specs(root, (), compress, matched_only=False)
44
- text = assemble_context(matched, '## Active Specs (path-mapped)') if matched else ''
45
- budget = config.get('budget', {})
46
- if not isinstance(budget, dict):
47
- raise SpecBudgetError('invalid_budget', 'budget must be a mapping')
48
- limit = min(int(budget.get('total', 2500)), int(budget.get('l1_max', 1700)))
49
- if limit <= 0:
50
- raise SpecBudgetError('invalid_budget', 'budget limit must be positive')
51
- total = estimate_tokens(text)
52
- return {'matched_specs': [{'path': item['path'], 'tokens': estimate_tokens(item['content'])}
53
- for item in matched], 'matched_total': total,
54
- 'aggregate_total': sum(estimate_tokens(item['content']) for item in aggregate),
55
- 'limit': limit, 'over_limit': total > limit}