@michaelxwb/code-flow 0.6.4 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -40
  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 +67 -35
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
  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 -40
  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 +67 -35
  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 +73 -34
  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 -40
  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 +67 -35
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
  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 -40
  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 +68 -41
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
  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 -1714
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
  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 -380
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
  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} +37 -3
  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} +252 -26
  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 -299
  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,12 +1,14 @@
1
1
  #!/usr/bin/env python3
2
2
  import json
3
+ import glob
3
4
  import os
4
5
  import re
5
6
  import sys
7
+ from pathlib import Path
6
8
 
7
- import cf_log
8
- from cf_checks import load_check_state, parse_spec_checks
9
- from cf_core import (
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 (
10
12
  build_effective_mapping,
11
13
  build_spec_catalog,
12
14
  estimate_tokens,
@@ -86,14 +88,30 @@ def normalize_line(line: str) -> str:
86
88
  return " ".join(line.strip().split())
87
89
 
88
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
+
89
107
  def find_redundant_lines(specs: list) -> dict:
90
108
  line_map: dict = {}
91
109
  for spec in specs:
92
- for raw in spec["content"].splitlines():
110
+ for raw in _audit_body(spec["content"]).splitlines():
93
111
  line = normalize_line(raw)
94
112
  if not line:
95
113
  continue
96
- if line.startswith("#") or line == "---":
114
+ if line.startswith("#") or line == "---" or _YAML_STRUCTURE.fullmatch(line):
97
115
  continue
98
116
  if len(line) < 6:
99
117
  continue
@@ -102,23 +120,73 @@ def find_redundant_lines(specs: list) -> dict:
102
120
  return redundant
103
121
 
104
122
 
105
- def find_missing_paths(text: str, project_root: str, base_dir: str = "") -> list:
106
- """路径引用检查:项目根解析失败时,再按 spec 自身目录二次解析——
107
- _map.md 常用相对本域目录的引用(如 design/design-lite.md)。"""
108
- missing: list = []
109
- for token in set(PATH_PATTERN.findall(text)):
110
- if token.startswith("http://") or token.startswith("https://"):
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})
111
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]
112
161
  if os.path.isabs(token):
113
- if not os.path.exists(token):
114
- missing.append(token)
115
- continue
116
- if os.path.exists(os.path.join(project_root, token)):
117
- continue
118
- if base_dir and os.path.exists(os.path.join(base_dir, 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 "*?{}[]"):
119
181
  continue
120
- missing.append(token)
121
- return sorted(missing)
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]
122
190
 
123
191
 
124
192
  def build_report(project_root: str, platform: str = "") -> dict:
@@ -165,6 +233,8 @@ def build_report(project_root: str, platform: str = "") -> dict:
165
233
  # 模板标记前置:模板(tags:[])不参与冗长/冗余审计,也不把
166
234
  # 模板间共享的文档头计入其他文件的冗余计数
167
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)
168
238
  effective_mapping = build_effective_mapping(
169
239
  project_root, config.get("path_mapping") or {}
170
240
  )
@@ -176,11 +246,11 @@ def build_report(project_root: str, platform: str = "") -> dict:
176
246
 
177
247
  for spec in specs:
178
248
  issues = []
179
- if not spec["template"] and spec["tokens"] > 500:
249
+ if not spec["template"] and spec["tokens"] > long_spec_tokens:
180
250
  issues.append(f"冗长: {spec['tokens']} tokens")
181
251
 
182
252
  spec_dir = os.path.join(specs_root, os.path.dirname(spec.get("rel", "")))
183
- missing_paths = find_missing_paths(spec["content"], project_root, spec_dir)
253
+ missing_paths, resolved_paths = _path_resolution(spec["content"], project_root, spec_dir, roots)
184
254
  for path in missing_paths[:3]:
185
255
  issues.append(f"过时: 路径不存在 {path}")
186
256
 
@@ -206,6 +276,7 @@ def build_report(project_root: str, platform: str = "") -> dict:
206
276
  "tokens": spec["tokens"],
207
277
  "issues": issues,
208
278
  "template": spec["template"],
279
+ "resolved_paths": resolved_paths,
209
280
  })
210
281
 
211
282
  for entry in files:
@@ -245,6 +316,7 @@ def build_report(project_root: str, platform: str = "") -> dict:
245
316
  "catalog": catalog_info,
246
317
  "review": review,
247
318
  "warnings": [],
319
+ "audit_warnings": audit_warnings,
248
320
  }
249
321
 
250
322
 
@@ -0,0 +1,55 @@
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}