@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,387 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Schema-v1 Spec metadata parser and deterministic content hashes."""
3
-
4
- from __future__ import annotations
5
-
6
- from dataclasses import dataclass
7
- import hashlib
8
- import json
9
- from pathlib import Path
10
- import re
11
- from typing import Mapping, Optional
12
-
13
-
14
-
15
- ALLOWED_STAGES = frozenset(("prd", "design", "plan", "code", "review"))
16
- ALLOWED_ENFORCEMENT = frozenset(("required", "advisory"))
17
- ALLOWED_VERIFIERS = frozenset(("document", "regex", "ast", "command", "test", "manual"))
18
- ALLOWED_VERIFIER_STAGES = frozenset(("code", "review"))
19
- _ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
20
- _RULE_ID_RE = re.compile(r"^RULE-[a-z0-9]+(?:-[a-z0-9]+)*-\d{3}$")
21
- _EXPLICIT_RULE_RE = re.compile(r"^\[(RULE-[a-z0-9-]+-\d{3})\]\s+(.+)$")
22
- _FRONTMATTER_RE = re.compile(r"\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|$)", re.DOTALL)
23
- _FRONTMATTER_OPEN_RE = re.compile(br"---[ \t]*\r?\n")
24
- _FRONTMATTER_CLOSE_RE = re.compile(br"---[ \t]*(?:\r?\n)?")
25
- _SECTION_RE = re.compile(r"^##\s+(Rules|Anti-Patterns|Patterns|Examples)\s*$")
26
- _H2_RE = re.compile(r"^##\s+")
27
- _BULLET_RE = re.compile(r"^\s*-\s+(.+\S|\S)\s*$")
28
-
29
-
30
- class SpecMetadataError(ValueError):
31
- """A schema error with a stable field and source location."""
32
-
33
- def __init__(self, path: str, field: str, line: int, message: str) -> None:
34
- self.path = path
35
- self.field = field
36
- self.line = max(1, line)
37
- self.message = message
38
- super().__init__(f"{path}:{self.line}: metadata_error {field}: {message}")
39
-
40
-
41
- @dataclass(frozen=True)
42
- class SpecHashes:
43
- file_sha256: str
44
- metadata_sha256: str
45
- rules_sha256: str
46
-
47
- def values(self) -> tuple[str, str, str]:
48
- return (self.file_sha256, self.metadata_sha256, self.rules_sha256)
49
-
50
-
51
- @dataclass(frozen=True)
52
- class SpecVerifier:
53
- rule: str
54
- type: str
55
- config: Mapping[str, object]
56
- stage: str = "code"
57
- files: tuple[str, ...] = ()
58
-
59
-
60
- @dataclass(frozen=True)
61
- class SpecRule:
62
- ref: str
63
- text: str
64
- section: str
65
- enforcement: str
66
- text_sha256: str
67
- line: int
68
-
69
-
70
- @dataclass(frozen=True)
71
- class SpecHeader:
72
- id: str
73
- stages: tuple[str, ...]
74
- enforcement: str
75
-
76
-
77
- @dataclass(frozen=True)
78
- class SpecMetadata:
79
- id: str
80
- description: str
81
- stages: tuple[str, ...]
82
- enforcement: str
83
- owner: Optional[str]
84
- checks: tuple[Mapping[str, object], ...]
85
- verifiers: tuple[SpecVerifier, ...]
86
- rules: tuple[SpecRule, ...]
87
- hashes: SpecHashes
88
- path: str
89
-
90
-
91
- def _sha256(data: bytes) -> str:
92
- return hashlib.sha256(data).hexdigest()
93
-
94
-
95
- def _normalized_text(text: str) -> str:
96
- return " ".join(text.split())
97
-
98
-
99
- def _canonical_json(value: object, path: str, field: str, line: int) -> bytes:
100
- try:
101
- text = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
102
- except (TypeError, ValueError) as exc:
103
- raise SpecMetadataError(path, field, line, f"无法规范化: {exc}") from exc
104
- return text.encode("utf-8")
105
-
106
-
107
- def _field_line(frontmatter: str, key: str) -> int:
108
- pattern = re.compile(rf"^\s*{re.escape(key)}\s*:")
109
- for number, line in enumerate(frontmatter.splitlines(), start=2):
110
- if pattern.match(line):
111
- return number
112
- return 1
113
-
114
-
115
- def _verifier_field_line(frontmatter: str, index: int, key: str) -> int:
116
- current = -1
117
- for number, line in enumerate(frontmatter.splitlines(), start=2):
118
- if re.match(r"^\s*-\s+rule\s*:", line):
119
- current += 1
120
- if current == index and re.match(rf"^\s+{re.escape(key)}\s*:", line):
121
- return number
122
- return _field_line(frontmatter, "verifiers")
123
-
124
-
125
- def _split_frontmatter(content: str, path: str) -> tuple[Mapping[str, object], str, str]:
126
- match = _FRONTMATTER_RE.match(content)
127
- if not match:
128
- raise SpecMetadataError(path, "frontmatter", 1, "缺失或未闭合 YAML frontmatter")
129
- raw = match.group(1)
130
- return _parse_frontmatter(raw, path), raw, content[match.end():]
131
-
132
-
133
- def _parse_frontmatter(raw: str, path: str) -> Mapping[str, object]:
134
- import yaml
135
- try:
136
- loaded = yaml.safe_load(raw) or {}
137
- except yaml.YAMLError as exc:
138
- mark = getattr(exc, "problem_mark", None)
139
- line = (mark.line + 2) if mark is not None else 1
140
- raise SpecMetadataError(path, "frontmatter", line, f"YAML 解析失败: {exc}") from exc
141
- if not isinstance(loaded, dict):
142
- raise SpecMetadataError(path, "frontmatter", 2, "顶层必须是 mapping")
143
- if not all(isinstance(key, str) for key in loaded):
144
- raise SpecMetadataError(path, "frontmatter", 2, "所有顶层 key 必须是字符串")
145
- return loaded
146
-
147
-
148
- def _required_string(meta: Mapping[str, object], key: str, path: str, raw: str) -> str:
149
- value = meta.get(key)
150
- if not isinstance(value, str) or not value.strip():
151
- raise SpecMetadataError(path, key, _field_line(raw, key), "必须是非空字符串")
152
- return value.strip()
153
-
154
-
155
- def _validate_stages(meta: Mapping[str, object], path: str, raw: str) -> tuple[str, ...]:
156
- value = meta.get("stages")
157
- line = _field_line(raw, "stages")
158
- if not isinstance(value, list) or not value or not all(isinstance(item, str) for item in value):
159
- raise SpecMetadataError(path, "stages", line, "必须是非空字符串列表")
160
- stages = tuple(item.strip() for item in value)
161
- invalid = sorted(set(stages) - ALLOWED_STAGES)
162
- if invalid or len(set(stages)) != len(stages):
163
- detail = f"非法或重复 stage: {invalid or list(stages)}"
164
- raise SpecMetadataError(path, "stages", line, detail)
165
- return stages
166
-
167
-
168
- def _spec_header(meta: Mapping[str, object], raw: str, path: str) -> SpecHeader:
169
- spec_id = _required_string(meta, "id", path, raw)
170
- if not _ID_RE.fullmatch(spec_id):
171
- raise SpecMetadataError(path, "id", _field_line(raw, "id"), "必须是 kebab-case")
172
- stages = _validate_stages(meta, path, raw)
173
- enforcement = _required_string(meta, "enforcement", path, raw)
174
- if enforcement not in ALLOWED_ENFORCEMENT:
175
- line = _field_line(raw, "enforcement")
176
- raise SpecMetadataError(path, "enforcement", line, f"必须是 {sorted(ALLOWED_ENFORCEMENT)}")
177
- return SpecHeader(spec_id, stages, enforcement)
178
-
179
-
180
- def _read_frontmatter(path: str) -> str:
181
- lines: list[bytes] = []
182
- try:
183
- with Path(path).open("rb") as source:
184
- if not _FRONTMATTER_OPEN_RE.fullmatch(source.readline()):
185
- raise SpecMetadataError(path, "frontmatter", 1, "缺失或未闭合 YAML frontmatter")
186
- for line in source:
187
- if _FRONTMATTER_CLOSE_RE.fullmatch(line):
188
- break
189
- lines.append(line)
190
- else:
191
- raise SpecMetadataError(path, "frontmatter", 1, "缺失或未闭合 YAML frontmatter")
192
- except OSError as exc:
193
- raise SpecMetadataError(path, "file", 1, f"读取失败: {exc}") from exc
194
- raw = b"".join(lines)
195
- try:
196
- return raw.decode("utf-8")
197
- except UnicodeDecodeError as exc:
198
- line = raw[:exc.start].count(b"\n") + 2
199
- raise SpecMetadataError(path, "encoding", line, f"必须是 UTF-8: {exc}") from exc
200
-
201
-
202
- def load_spec_header(path: str) -> SpecHeader:
203
- """Load only routing fields, stopping at the frontmatter delimiter."""
204
- raw = _read_frontmatter(path)
205
- return _spec_header(_parse_frontmatter(raw, path), raw, path)
206
-
207
-
208
- def _validate_checks(meta: Mapping[str, object], path: str, raw: str) -> tuple[Mapping[str, object], ...]:
209
- value = meta.get("checks", [])
210
- if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
211
- raise SpecMetadataError(path, "checks", _field_line(raw, "checks"), "必须是 mapping 列表")
212
- return tuple(value)
213
-
214
-
215
- def _parse_verifier_files(value: object, path: str, raw: str, index: int) -> tuple[str, ...]:
216
- line = _verifier_field_line(raw, index, "files")
217
- if not isinstance(value, list) or not value:
218
- raise SpecMetadataError(path, f"verifiers[{index}].files", line, "必须是非空 glob 字符串数组")
219
- files: list[str] = []
220
- for item in value:
221
- if not isinstance(item, str) or not item.strip():
222
- raise SpecMetadataError(path, f"verifiers[{index}].files", line, "glob 必须是非空字符串")
223
- text = item.strip().replace("\\", "/")
224
- if text.startswith("/") or re.match(r"^[A-Za-z]:/", text):
225
- raise SpecMetadataError(path, f"verifiers[{index}].files", line, f"禁止绝对路径: {item!r}")
226
- if ".." in text.split("/"):
227
- raise SpecMetadataError(path, f"verifiers[{index}].files", line, f"禁止 .. 跳出仓库: {item!r}")
228
- files.append(text)
229
- return tuple(files)
230
-
231
-
232
- def _parse_verifiers(
233
- meta: Mapping[str, object], path: str, raw: str, stages: tuple[str, ...]
234
- ) -> tuple[SpecVerifier, ...]:
235
- value = meta.get("verifiers", [])
236
- if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
237
- raise SpecMetadataError(path, "verifiers", _field_line(raw, "verifiers"), "必须是 mapping 列表")
238
- result: list[SpecVerifier] = []
239
- seen: set[str] = set()
240
- for index, item in enumerate(value):
241
- rule = item.get("rule")
242
- kind = item.get("type")
243
- config = item.get("config")
244
- if not isinstance(rule, str) or not _RULE_ID_RE.fullmatch(rule):
245
- line = _verifier_field_line(raw, index, "rule")
246
- raise SpecMetadataError(path, f"verifiers[{index}].rule", line, "必须是稳定 RULE ID")
247
- if kind not in ALLOWED_VERIFIERS:
248
- line = _verifier_field_line(raw, index, "type")
249
- raise SpecMetadataError(path, f"verifiers[{index}].type", line, f"不支持 type: {kind!r}")
250
- if not isinstance(config, dict):
251
- line = _verifier_field_line(raw, index, "config")
252
- raise SpecMetadataError(path, f"verifiers[{index}].config", line, "必须是 mapping")
253
- explicit_stage = "stage" in item
254
- # manual(人工验收)默认跟随 spec 声明的 review 阶段,落在需求级终验一次性确认;
255
- # spec 未声明 review 时保持 code(逐任务确认);显式 stage 始终优先。
256
- default_stage = "review" if kind == "manual" and "review" in stages else "code"
257
- stage = item.get("stage", default_stage)
258
- if explicit_stage and (not isinstance(stage, str) or stage not in ALLOWED_VERIFIER_STAGES):
259
- line = _verifier_field_line(raw, index, "stage")
260
- raise SpecMetadataError(path, f"verifiers[{index}].stage", line, f"必须是 {sorted(ALLOWED_VERIFIER_STAGES)} 之一")
261
- if explicit_stage and stage not in stages:
262
- line = _verifier_field_line(raw, index, "stage")
263
- raise SpecMetadataError(path, f"verifiers[{index}].stage", line, f"stage {stage!r} 未在 stages 声明: {', '.join(stages)}")
264
- files = _parse_verifier_files(item["files"], path, raw, index) if "files" in item else ()
265
- if rule in seen:
266
- raise SpecMetadataError(path, "verifiers", _field_line(raw, "verifiers"), f"重复 verifier: {rule}")
267
- seen.add(rule)
268
- result.append(SpecVerifier(rule=rule, type=kind, config=config, stage=stage, files=files))
269
- return tuple(result)
270
-
271
-
272
- def _domain_from_path(path: str) -> str:
273
- source = Path(path)
274
- parts = source.parts
275
- if "specs" in parts:
276
- index = parts.index("specs")
277
- candidate = parts[index + 1] if index + 1 < len(parts) - 1 else source.stem
278
- else:
279
- candidate = source.parent.name or source.stem
280
- normalized = re.sub(r"[^a-z0-9]+", "-", candidate.lower()).strip("-")
281
- return normalized or "spec"
282
-
283
-
284
- def _entry_details(section: str) -> tuple[str, str]:
285
- if section in ("Rules", "Anti-Patterns"):
286
- return "RULE", "required"
287
- if section == "Patterns":
288
- return "PATTERN", "advisory"
289
- return "EXAMPLE", "informational"
290
-
291
-
292
- def _parse_rules(body: str, path: str, line_offset: int) -> tuple[SpecRule, ...]:
293
- domain = _domain_from_path(path)
294
- counters = {"RULE": 0, "PATTERN": 0, "EXAMPLE": 0}
295
- rules: list[SpecRule] = []
296
- section = ""
297
- fence = ""
298
- used: set[str] = set()
299
- for relative_line, line in enumerate(body.splitlines(), start=1):
300
- number = relative_line + line_offset
301
- stripped = line.strip()
302
- if not fence and stripped.startswith(("```", "~~~")):
303
- marker = stripped[:3]
304
- fence = marker
305
- continue
306
- if fence and stripped.startswith(fence):
307
- fence = ""
308
- continue
309
- heading = _SECTION_RE.match(line) if not fence else None
310
- if heading:
311
- section = heading.group(1)
312
- continue
313
- if not fence and _H2_RE.match(line):
314
- section = ""
315
- continue
316
- bullet = _BULLET_RE.match(line) if section and not fence else None
317
- if not bullet:
318
- continue
319
- prefix, enforcement = _entry_details(section)
320
- counters[prefix] += 1
321
- ref = f"{prefix}-{domain}-{counters[prefix]:03d}"
322
- text = bullet.group(1).strip()
323
- explicit = _EXPLICIT_RULE_RE.match(text) if prefix == "RULE" else None
324
- if prefix == "RULE" and text.startswith("[RULE-") and not explicit:
325
- raise SpecMetadataError(path, "rules", number, "显式 Rule ID 必须是 RULE-<domain>-NNN")
326
- if explicit:
327
- ref, text = explicit.group(1), explicit.group(2).strip()
328
- while ref in used and not explicit:
329
- counters[prefix] += 1
330
- ref = f"{prefix}-{domain}-{counters[prefix]:03d}"
331
- if ref in used:
332
- raise SpecMetadataError(path, "rules", number, f"重复规则 ID: {ref}")
333
- used.add(ref)
334
- normalized = _normalized_text(text)
335
- rules.append(SpecRule(ref, normalized, section, enforcement, _sha256(normalized.encode()), number))
336
- return tuple(rules)
337
-
338
-
339
- def _validate_links(
340
- rules: tuple[SpecRule, ...], verifiers: tuple[SpecVerifier, ...], path: str, raw: str
341
- ) -> None:
342
- required = {rule.ref for rule in rules if rule.enforcement == "required"}
343
- linked = {verifier.rule for verifier in verifiers}
344
- missing = sorted(required - linked)
345
- unknown = sorted(linked - required)
346
- line = _field_line(raw, "verifiers")
347
- if missing:
348
- raise SpecMetadataError(path, "verifiers", line, f"required Rule 缺 verifier: {', '.join(missing)}")
349
- if unknown:
350
- raise SpecMetadataError(path, "verifiers", line, f"verifier 引用未知 required Rule: {', '.join(unknown)}")
351
-
352
-
353
- def parse_spec_metadata(content: str, path: str = "<memory>", raw_bytes: Optional[bytes] = None) -> SpecMetadata:
354
- meta, raw_frontmatter, body = _split_frontmatter(content, path)
355
- header = _spec_header(meta, raw_frontmatter, path)
356
- description = _required_string(meta, "description", path, raw_frontmatter)
357
- owner = meta.get("owner")
358
- if owner is not None and (not isinstance(owner, str) or not owner.strip()):
359
- raise SpecMetadataError(path, "owner", _field_line(raw_frontmatter, "owner"), "必须是非空字符串")
360
- checks = _validate_checks(meta, path, raw_frontmatter)
361
- verifiers = _parse_verifiers(meta, path, raw_frontmatter, header.stages)
362
- body_line_offset = raw_frontmatter.count("\n") + 3
363
- rules = _parse_rules(body, path, body_line_offset)
364
- _validate_links(rules, verifiers, path, raw_frontmatter)
365
- required_rules = [(rule.ref, rule.text) for rule in rules if rule.enforcement == "required"]
366
- source_bytes = raw_bytes if raw_bytes is not None else content.encode("utf-8")
367
- hashes = SpecHashes(
368
- file_sha256=_sha256(source_bytes),
369
- metadata_sha256=_sha256(_canonical_json(meta, path, "frontmatter", 1)),
370
- rules_sha256=_sha256(_canonical_json(required_rules, path, "rules", 1)),
371
- )
372
- return SpecMetadata(
373
- header.id, description, header.stages, header.enforcement,
374
- owner, checks, verifiers, rules, hashes, path,
375
- )
376
-
377
-
378
- def load_spec_metadata(path: str) -> SpecMetadata:
379
- try:
380
- raw = Path(path).read_bytes()
381
- except OSError as exc:
382
- raise SpecMetadataError(path, "file", 1, f"读取失败: {exc}") from exc
383
- try:
384
- content = raw.decode("utf-8")
385
- except UnicodeDecodeError as exc:
386
- raise SpecMetadataError(path, "encoding", 1, f"必须是 UTF-8: {exc}") from exc
387
- return parse_spec_metadata(content, path=path, raw_bytes=raw)