@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,193 +0,0 @@
1
- """Read-only, input-bound Spec optimization proposals."""
2
- from __future__ import annotations
3
-
4
- import hashlib
5
- import difflib
6
- import json
7
- from pathlib import Path
8
- import re
9
- from typing import Sequence
10
-
11
- from codeflow.core.cf_core import estimate_tokens, load_config
12
- from codeflow.spec.cf_spec_budget import measure_path_budget
13
- from codeflow.spec.cf_spec_impact import scan_impacts
14
- from codeflow.spec.cf_spec_metadata import parse_spec_metadata
15
- from codeflow.spec.cf_spec_resolver import resolve_candidates
16
-
17
-
18
- def _selected(root: str, paths: Sequence[str]) -> list:
19
- all_candidates = list(resolve_candidates(root, 'code', ()))
20
- if not paths:
21
- return all_candidates
22
- normalized = [item.replace('\\', '/').removeprefix('.code-flow/specs/') for item in paths]
23
- direct = {item.path for item in all_candidates if item.path in normalized}
24
- code_paths = [path for path in normalized if path not in direct]
25
- matched = {item.path for item in resolve_candidates(root, 'code', tuple(code_paths), matched_only=True)} if code_paths else set()
26
- selected = [item for item in all_candidates if item.path in direct | matched]
27
- if not selected:
28
- raise ValueError('no matching specs for requested paths')
29
- return selected
30
-
31
-
32
- def _section_tokens(text: str, heading: str) -> int:
33
- match = re.search(rf'(?im)^##\s+{re.escape(heading)}\s*$([\s\S]*?)(?=^##\s|\Z)', text)
34
- return estimate_tokens(match.group(1)) if match else 0
35
-
36
-
37
- def _spec_data(root: Path, candidate: object, config: dict) -> dict[str, object]:
38
- relative = str(candidate.path)
39
- base = (root / '.code-flow/specs').resolve()
40
- path = (base / relative).resolve()
41
- if base not in path.parents or not path.is_file():
42
- raise ValueError(f'invalid spec path: {relative}')
43
- raw = path.read_bytes()
44
- text = raw.decode('utf-8')
45
- domain = relative.split('/', 1)[0]
46
- mapping = config.get('path_mapping') or {}
47
- entry = mapping.get(domain, {}) if isinstance(mapping, dict) else {}
48
- patterns = entry.get('patterns', []) if isinstance(entry, dict) else []
49
- rules = [rule for rule in candidate.metadata.rules if rule.ref.startswith('RULE-')]
50
- tokens = estimate_tokens(text)
51
- rule_tokens = _section_tokens(text, 'Rules')
52
- convention_tokens = _section_tokens(text, 'Conventions')
53
- return {'id': candidate.spec_id, 'path': relative, 'sha256': hashlib.sha256(raw).hexdigest(),
54
- 'tokens': tokens, 'rule_tokens': rule_tokens, 'convention_tokens': convention_tokens,
55
- 'rule_percent': round(100 * rule_tokens / tokens, 2) if tokens else 0,
56
- 'convention_percent': round(100 * convention_tokens / tokens, 2) if tokens else 0,
57
- 'rule_ids': sorted(rule.ref for rule in rules), 'patterns': patterns,
58
- 'verifiers': [{'rule': item.rule, 'type': item.type, 'stage': item.stage, 'config': item.config}
59
- for item in candidate.metadata.verifiers]}
60
-
61
-
62
- def _suggestions(specs: list[dict[str, object]], limit: int) -> list[dict[str, object]]:
63
- return [{'path': item['path'], 'kind': 'review_conventions_split',
64
- 'reason': '规范超过单次注入上限;需人工选择目标,规则保持原位'}
65
- for item in specs if int(item['tokens']) > limit and int(item['convention_tokens']) > 0]
66
-
67
-
68
- def _overlaps(specs: list[dict[str, object]]) -> list[dict[str, object]]:
69
- groups: list[dict[str, object]] = []
70
- for index, first in enumerate(specs):
71
- for second in specs[index + 1:]:
72
- shared = sorted(set(first['patterns']) & set(second['patterns']))
73
- if shared:
74
- groups.append({'paths': [first['path'], second['path']], 'patterns': shared})
75
- return groups
76
-
77
-
78
- def _target_path(root: Path, target: str) -> Path:
79
- relative = Path(target)
80
- if relative.is_absolute() or '..' in relative.parts or len(relative.parts) < 2 or relative.suffix != '.md':
81
- raise ValueError('target must be a relative spec path inside an existing domain')
82
- if relative.parts[0] in ('shared', '_session') or relative.parts[0].startswith('.'):
83
- raise ValueError('target domain must be an injectable spec domain')
84
- base = (root / '.code-flow/specs').resolve()
85
- path = base / relative
86
- if any((base.joinpath(*relative.parts[:i])).is_symlink() for i in range(1, len(relative.parts) + 1)):
87
- raise ValueError('target path must not contain symlinks')
88
- if base not in path.resolve().parents or not path.parent.is_dir():
89
- raise ValueError('target domain directory must exist within specs')
90
- if path.exists() or path.is_symlink():
91
- raise ValueError('target already exists')
92
- return path
93
-
94
-
95
- def _movement(root: Path, specs: list[dict[str, object]], target: str, config: dict) -> tuple[list[dict], list[dict]]:
96
- if len(specs) != 1:
97
- raise ValueError('target requires exactly one source spec')
98
- destination = _target_path(root, target)
99
- source = specs[0]
100
- base = root / '.code-flow/specs'
101
- relative_source = Path(str(source['path']))
102
- if any(base.joinpath(*relative_source.parts[:i]).is_symlink() for i in range(1, len(relative_source.parts) + 1)):
103
- raise ValueError('source path must not contain symlinks')
104
- text = (base / relative_source).read_text(encoding='utf-8')
105
- match = re.search(r'(?im)^##\s+Conventions\s*$([\s\S]*?)(?=^##\s|\Z)', text)
106
- if not match or 'RULE-' in match.group(0):
107
- raise ValueError('source conventions missing or contain rule references')
108
- new_id = str(source['id']) + '-conventions'
109
- for stage in ('prd', 'design', 'plan', 'code', 'review'):
110
- if any(item.spec_id == new_id for item in resolve_candidates(str(root), stage, ())):
111
- raise ValueError('target spec id already exists')
112
- relative = destination.relative_to(root / '.code-flow/specs').as_posix()
113
- operation = {'kind': 'move_conventions', 'source': source['path'], 'target': relative, 'target_id': new_id}
114
- source_after, target_after = render_movement(root, operation)
115
- operation.update({'source_after_sha256': hashlib.sha256(source_after).hexdigest(),
116
- 'target_after_sha256': hashlib.sha256(target_after).hexdigest(),
117
- 'source_after_tokens': estimate_tokens(source_after.decode('utf-8')),
118
- 'target_after_tokens': estimate_tokens(target_after.decode('utf-8')),
119
- 'source_diff': ''.join(difflib.unified_diff(text.splitlines(keepends=True),
120
- source_after.decode('utf-8').splitlines(keepends=True), fromfile=str(source['path']),
121
- tofile=str(source['path']))),
122
- 'target_diff': ''.join(difflib.unified_diff([], target_after.decode('utf-8').splitlines(keepends=True),
123
- fromfile='/dev/null', tofile=relative))})
124
- domain = relative.split('/')[0]
125
- mapping = config.get('path_mapping') or {}
126
- entry = mapping.get(domain, {}) if isinstance(mapping, dict) else {}
127
- patterns = entry.get('patterns', []) if isinstance(entry, dict) else []
128
- return [operation], [{'kind': 'auto_discovery_add', 'domain': domain, 'path': relative,
129
- 'source_patterns': source['patterns'], 'target_patterns': patterns}]
130
-
131
-
132
- def render_movement(root: Path, operation: dict) -> tuple[bytes, bytes]:
133
- source = root / '.code-flow/specs' / str(operation['source'])
134
- text = source.read_text(encoding='utf-8')
135
- match = re.search(r'(?im)^##\s+Conventions\s*$([\s\S]*?)(?=^##\s|\Z)', text)
136
- if not match or 'RULE-' in match.group(0):
137
- raise ValueError('rule guard: invalid conventions section')
138
- updated = text[:match.start()] + text[match.end():]
139
- before = parse_spec_metadata(text, path=str(source))
140
- target = ('---\nid: ' + str(operation['target_id']) + '\ndescription: Migrated conventions\n'
141
- 'stages: ' + json.dumps(list(before.stages)) + '\nenforcement: advisory\n---\n\n'
142
- '# Conventions\n\n' + match.group(0) + '\n')
143
- after = parse_spec_metadata(updated, path=str(source))
144
- target_meta = parse_spec_metadata(target, path=str(root / '.code-flow/specs' / str(operation['target'])))
145
- signature = lambda meta: [(rule.ref, rule.text_sha256, rule.section, rule.enforcement) for rule in meta.rules]
146
- if signature(before) != signature(after) or before.verifiers != after.verifiers or target_meta.verifiers:
147
- raise ValueError('rule guard: rule or verifier changed')
148
- if any(item.ref.startswith('RULE-') for item in target_meta.rules):
149
- raise ValueError('rule guard: target contains rules')
150
- return updated.encode('utf-8'), target.encode('utf-8')
151
-
152
-
153
- def build_proposal(root: str, paths: Sequence[str], target: str = '') -> dict[str, object]:
154
- project = Path(root).resolve()
155
- config = load_config(root)
156
- candidates = _selected(root, paths)
157
- specs = [_spec_data(project, candidate, config) for candidate in candidates]
158
- operations, mapping_diff = _movement(project, specs, target, config) if target else ([], [])
159
- identities = {str(item['id']): str(item['path']) for item in specs}
160
- for operation in operations:
161
- identities[str(operation['target_id'])] = str(operation['target'])
162
- impact = scan_impacts(root, identities)
163
- if impact['truncated'] or impact['errors']:
164
- raise ValueError('impact scan incomplete; proposal cannot assert complete scope')
165
- budget = config.get('budget') or {}
166
- if not isinstance(budget, dict):
167
- raise ValueError('budget must be a mapping')
168
- limit = min(int(budget.get('total', 2500)), int(budget.get('l1_max', 1700)))
169
- if limit <= 0:
170
- raise ValueError('budget limit must be positive')
171
- config_hash = hashlib.sha256((project / '.code-flow/config.yml').read_bytes()).hexdigest()
172
- code_paths = [path for path in paths if not any(path in (item['path'], '.code-flow/specs/' + str(item['path'])) for item in specs)]
173
- measured = measure_path_budget(root, code_paths) if code_paths else None
174
- raw_before = sum(int(item['tokens']) for item in specs)
175
- raw_after = (raw_before - int(specs[0]['tokens']) + int(operations[0]['source_after_tokens'])
176
- + int(operations[0]['target_after_tokens'])) if operations else raw_before
177
- affected_files = {str(row.get('context_path') or row.get('task_path')) for row in impact['references']}
178
- affected_hashes = {path: hashlib.sha256((project / path).read_bytes()).hexdigest() for path in sorted(affected_files)}
179
- overlap = _overlaps(specs)
180
- suggestions = _suggestions(specs, limit)
181
- suggestions.extend({'paths': group['paths'], 'kind': 'review_overlap',
182
- 'reason': '相同路径模式命中多份规范;人工评估是否合并 Conventions'} for group in overlap)
183
- proposal = {'schema': 1, 'config_sha256': config_hash, 'specs': specs,
184
- 'budget': {'before': {'raw_spec_tokens': raw_before, 'path_measurement': measured},
185
- 'after': {'raw_spec_tokens': raw_after, 'path_measurement': None if operations else measured},
186
- 'limit': limit, 'note': '变更后单次注入需在应用后重新测量'},
187
- 'mapping_diff': mapping_diff, 'affected_contexts': impact['references'],
188
- 'affected_file_hashes': affected_hashes,
189
- 'scanned_files': impact['scanned_files'], 'path_overlaps': overlap, 'suggestions': suggestions,
190
- 'operations': operations, 'note': ('请审核提案并交互确认应用。' if target
191
- else '未指定迁移目标,提案没有可应用的文件变更。')}
192
- proposal['proposal_id'] = hashlib.sha256(json.dumps(proposal, ensure_ascii=False, sort_keys=True).encode()).hexdigest()
193
- return proposal
@@ -1,187 +0,0 @@
1
- """Guarded, recoverable application of a reviewed conventions proposal."""
2
- from __future__ import annotations
3
-
4
- import hashlib
5
- import json
6
- import os
7
- from pathlib import Path
8
- import shutil
9
- from typing import Mapping
10
-
11
- from codeflow.spec.cf_spec_optimize import build_proposal, render_movement
12
- from codeflow.task.cf_task_doc import TaskDocumentError, parse_task_document
13
-
14
-
15
- def _digest(data: bytes) -> str:
16
- return hashlib.sha256(data).hexdigest()
17
-
18
-
19
- def _proposal_hash(proposal: Mapping[str, object]) -> str:
20
- unsigned = {key: value for key, value in proposal.items() if key != 'proposal_id'}
21
- return _digest(json.dumps(unsigned, ensure_ascii=False, sort_keys=True).encode())
22
-
23
-
24
- def _check_context_tasks(root: Path, directory: str) -> None:
25
- task_directory = root / directory
26
- statuses: list[str] = []
27
- for path in sorted(task_directory.glob('*.md')):
28
- if path.name.endswith(('.prd.md', '.design.md', '.scenarios.md')):
29
- continue
30
- try:
31
- document = parse_task_document(path.read_text(encoding='utf-8'), source=str(path))
32
- except TaskDocumentError as exc:
33
- if exc.code == 'task_document_empty':
34
- continue
35
- raise
36
- statuses.extend(task.status for task in document.tasks)
37
- if not statuses or any(status not in ('done', 'verified') for status in statuses):
38
- raise ValueError('inflight_context: affected Context has incomplete or unproven tasks')
39
-
40
-
41
- def _check_inflight(root: str, proposal: Mapping[str, object]) -> None:
42
- refs = proposal.get('affected_contexts')
43
- if not isinstance(refs, list):
44
- raise ValueError('proposal impact evidence missing')
45
- tasks = [row for row in refs if isinstance(row, dict) and row.get('source') == 'task' and not row.get('archived')]
46
- contexts = [row for row in refs if isinstance(row, dict) and row.get('source') == 'context' and not row.get('archived')]
47
- if any(row.get('status') not in ('done', 'verified') for row in tasks):
48
- raise ValueError('inflight_context: affected task is not complete')
49
- directories = {str(Path(str(row.get('context_path', ''))).parent) for row in contexts}
50
- for directory in sorted(directories):
51
- _check_context_tasks(Path(root), directory)
52
-
53
-
54
- def _validate(root: str, proposal: Mapping[str, object]) -> dict[str, object]:
55
- if proposal.get('schema') != 1 or proposal.get('proposal_id') != _proposal_hash(proposal):
56
- raise ValueError('proposal guard: invalid proposal identity')
57
- operations = proposal.get('operations')
58
- if not isinstance(operations, list) or len(operations) != 1 or not isinstance(operations[0], dict):
59
- raise ValueError('proposal guard: exactly one explicit operation is required')
60
- operation = operations[0]
61
- if operation.get('kind') != 'move_conventions' or not isinstance(operation.get('source'), str) or not isinstance(operation.get('target'), str):
62
- raise ValueError('proposal guard: invalid operation')
63
- try:
64
- current = build_proposal(root, (operation['source'],), str(operation['target']))
65
- except (OSError, ValueError) as exc:
66
- raise ValueError(f'proposal_stale: {exc}') from exc
67
- if current != proposal:
68
- raise ValueError('proposal_stale: input, impact, or operation changed')
69
- _check_inflight(root, proposal)
70
- return operation
71
-
72
-
73
- def _write(path: Path, data: bytes, mode: int = 0o600) -> None:
74
- descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode)
75
- with os.fdopen(descriptor, 'wb') as handle:
76
- os.fchmod(handle.fileno(), mode)
77
- handle.write(data)
78
- handle.flush()
79
- os.fsync(handle.fileno())
80
-
81
-
82
- def _journal(directory: Path, data: dict[str, object]) -> None:
83
- temporary = directory / 'journal.tmp'
84
- if temporary.exists():
85
- raise ValueError('transaction journal temporary file already exists')
86
- _write(temporary, (json.dumps(data, ensure_ascii=False, sort_keys=True) + '\n').encode())
87
- os.replace(temporary, directory / 'journal.json')
88
- descriptor = os.open(directory, os.O_RDONLY)
89
- try:
90
- os.fsync(descriptor)
91
- finally:
92
- os.close(descriptor)
93
-
94
-
95
- def commit_replace(staged: Path, destination: Path) -> None:
96
- """Replace the source; create a new target exclusively without overwriting."""
97
- if staged.name == 'target.staged':
98
- os.link(staged, destination)
99
- else:
100
- os.replace(staged, destination)
101
-
102
-
103
- def _rollback(source: Path, target: Path, backup: Path, original_hash: str,
104
- source_after_hash: str, target_after_hash: str, target_created: bool) -> None:
105
- if target_created:
106
- if not target.is_file() or _digest(target.read_bytes()) != target_after_hash:
107
- raise ValueError('recovery_required: target changed during rollback')
108
- target.unlink()
109
- current = _digest(source.read_bytes()) if source.is_file() else ''
110
- if current == original_hash:
111
- return
112
- if current != source_after_hash or _digest(backup.read_bytes()) != original_hash:
113
- raise ValueError('recovery_required: source changed during rollback')
114
- os.replace(backup, source)
115
-
116
-
117
- def _remove_transaction(directory: Path, parent_existed: bool) -> None:
118
- parent = directory.parent
119
- shutil.rmtree(directory)
120
- if not parent_existed:
121
- parent.rmdir()
122
-
123
-
124
- def _transaction(root: Path, proposal: Mapping[str, object], operation: Mapping[str, object]) -> dict[str, object]:
125
- base = root / '.code-flow/specs'
126
- source = base / str(operation['source'])
127
- target = base / str(operation['target'])
128
- original = source.read_bytes()
129
- after_source, after_target = render_movement(root, dict(operation))
130
- specs = proposal.get('specs')
131
- if (not isinstance(specs, list) or len(specs) != 1 or not isinstance(specs[0], dict)
132
- or _digest(original) != specs[0].get('sha256')
133
- or _digest(after_source) != operation.get('source_after_sha256')
134
- or _digest(after_target) != operation.get('target_after_sha256')):
135
- raise ValueError('proposal_stale: source or proposed result changed')
136
- parent = base / '.optimize-transactions'
137
- if parent.is_symlink() or (parent.exists() and (not parent.is_dir() or any(parent.iterdir()))):
138
- raise ValueError('recovery_required: unfinished optimization transaction')
139
- parent_existed = parent.exists()
140
- parent.mkdir(exist_ok=True)
141
- directory = parent / str(proposal['proposal_id'])
142
- directory.mkdir()
143
- backup, source_stage, target_stage = directory / 'source.backup', directory / 'source.staged', directory / 'target.staged'
144
- target_created = False
145
- state = {'schema': 1, 'proposal_id': proposal['proposal_id'], 'source': operation['source'],
146
- 'target': operation['target'], 'original_sha256': _digest(original),
147
- 'source_after_sha256': _digest(after_source), 'target_after_sha256': _digest(after_target), 'state': 'prepared'}
148
- try:
149
- _write(backup, original, source.stat().st_mode & 0o777)
150
- _write(source_stage, after_source, source.stat().st_mode & 0o777)
151
- _write(target_stage, after_target, 0o644)
152
- _journal(directory, state)
153
- state['state'] = 'committing'
154
- _journal(directory, state)
155
- if _digest(source.read_bytes()) != state['original_sha256']:
156
- raise ValueError('proposal_stale: source changed before commit')
157
- commit_replace(source_stage, source)
158
- commit_replace(target_stage, target)
159
- target_created = True
160
- if _digest(source.read_bytes()) != state['source_after_sha256'] or _digest(target.read_bytes()) != state['target_after_sha256']:
161
- raise ValueError('post-write guard: content differs from approved proposal')
162
- state['state'] = 'committed'
163
- _journal(directory, state)
164
- except (OSError, ValueError):
165
- _rollback(source, target, backup, _digest(original), _digest(after_source), _digest(after_target), target_created)
166
- _remove_transaction(directory, parent_existed)
167
- raise
168
- _remove_transaction(directory, parent_existed)
169
- return {'decision': 'pass', 'proposal_id': proposal['proposal_id'],
170
- 'changed': [str(operation['source']), str(operation['target'])]}
171
-
172
-
173
- def apply_proposal(root: str, proposal: Mapping[str, object], *, confirmed: bool) -> dict[str, object]:
174
- if not confirmed:
175
- raise ValueError('explicit confirmation required')
176
- project = Path(root).resolve()
177
- lock = project / '.code-flow/specs/.optimize.lock'
178
- try:
179
- descriptor = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
180
- except FileExistsError as exc:
181
- raise ValueError('recovery_required: optimization lock exists') from exc
182
- os.close(descriptor)
183
- try:
184
- operation = _validate(root, proposal)
185
- return _transaction(project, proposal, operation)
186
- finally:
187
- lock.unlink()