@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,326 +0,0 @@
1
- """Validate test registration against a bounded, real collection command."""
2
- from __future__ import annotations
3
-
4
- import json
5
- from pathlib import Path
6
- import re
7
- import shlex
8
- import time
9
- from typing import Mapping, Sequence
10
-
11
- from codeflow.acceptance.cf_acceptance_schema import scenario_check_ids, validate_checks
12
- from codeflow.core.cf_exec_base import run_command
13
-
14
-
15
- REGISTRATION_FIELDS = ("suite", "test_ids", "markers", "collect", "level", "environment_revision", "evidence_ttl_seconds")
16
-
17
-
18
- class CollectionError(ValueError):
19
- def __init__(self, code: str, details: Mapping[str, object]) -> None:
20
- super().__init__(code)
21
- self.code = code
22
- self.details = dict(details)
23
-
24
-
25
- def _pytest_markers(argv: Sequence[str]) -> list[str]:
26
- positions = [index for index, value in enumerate(argv[:-1]) if value == "-m" and argv[index + 1] != "pytest"]
27
- expression = argv[positions[-1] + 1] if positions else ""
28
- markers = [value.strip() for value in expression.split(" and ")] if expression else []
29
- if any(not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*", value) for value in markers):
30
- raise ValueError("check_collection_marker_expression_invalid")
31
- return markers
32
-
33
-
34
- def _pytest_collection(output: str, argv: Sequence[str]) -> list[dict[str, object]]:
35
- markers = _pytest_markers(argv)
36
- return [{"id": line.strip(), "markers": markers,
37
- "capabilities": ["browser"] if "browser" in markers else []}
38
- for line in output.splitlines() if re.fullmatch(r"[^\s]+\.py::[^\s]+", line.strip())]
39
-
40
-
41
- def _objects(value: object) -> list[Mapping[str, object]]:
42
- if not isinstance(value, list) or not all(isinstance(item, Mapping) for item in value):
43
- raise ValueError("check_collection_invalid_playwright_structure")
44
- return value
45
-
46
-
47
- def _browser_projects(config: Mapping[str, object]) -> set[str]:
48
- projects: set[str] = set()
49
- for project in _objects(config.get("projects", [])):
50
- metadata = project.get("metadata", {})
51
- capabilities = metadata.get("capabilities", []) if isinstance(metadata, Mapping) else None
52
- if not isinstance(capabilities, list) or not all(isinstance(value, str) for value in capabilities):
53
- raise ValueError("check_collection_invalid_project_capabilities")
54
- if "browser" in capabilities:
55
- projects.add(str(project.get("name", "")).lower())
56
- return projects | {"chromium", "firefox", "webkit", "browser"}
57
-
58
-
59
- def _playwright_spec(spec: Mapping[str, object], suite: Mapping[str, object], report_root: Path,
60
- root: str, projects: set[str]) -> list[dict[str, object]]:
61
- file = spec.get("file", suite.get("file", ""))
62
- tags = spec.get("tags", [])
63
- if not isinstance(file, str) or not isinstance(tags, list) or not all(isinstance(value, str) for value in tags):
64
- raise ValueError("check_collection_invalid_playwright_spec")
65
- if type(spec.get("line")) is not int:
66
- raise ValueError("check_collection_invalid_playwright_location")
67
- file = (report_root / file).resolve().relative_to(Path(root).resolve()).as_posix()
68
- tests: list[dict[str, object]] = []
69
- for case in _objects(spec.get("tests", [])):
70
- project = case.get("projectName", "")
71
- if not isinstance(project, str):
72
- raise ValueError("check_collection_invalid_playwright_project")
73
- tests.append({"id": f"{file}:{spec['line']}@{project}", "markers": [tag.lstrip("@") for tag in tags],
74
- "capabilities": ["browser"] if project.lower() in projects else []})
75
- return tests
76
-
77
-
78
- def _playwright_collection(data: Mapping[str, object], root: str) -> list[dict[str, object]]:
79
- tests: list[dict[str, object]] = []
80
- config = data.get("config", {})
81
- if not isinstance(config, Mapping) or not isinstance(config.get("rootDir", root), str):
82
- raise ValueError("check_collection_invalid_playwright_config")
83
- report_root = Path(str(config.get("rootDir", root)))
84
- projects = _browser_projects(config)
85
- pending = list(_objects(data.get("suites", [])))
86
- while pending:
87
- suite = pending.pop()
88
- if not isinstance(suite, Mapping):
89
- raise ValueError("check_collection_invalid_playwright_suite")
90
- pending.extend(_objects(suite.get("suites", [])))
91
- for spec in _objects(suite.get("specs", [])):
92
- tests.extend(_playwright_spec(spec, suite, report_root, root, projects))
93
- return tests
94
-
95
-
96
- def _parse_collection(output: str, config: Mapping[str, object], root: str) -> list[Mapping[str, object]]:
97
- fmt = config.get("format", "json")
98
- if fmt == "pytest":
99
- return _pytest_collection(output, config["argv"])
100
- try:
101
- data = json.loads(output)
102
- except json.JSONDecodeError as exc:
103
- raise ValueError("check_collection_invalid_json") from exc
104
- if not isinstance(data, Mapping):
105
- raise ValueError("check_collection_invalid_tests")
106
- tests = _playwright_collection(data, root) if fmt == "playwright" else data.get("tests")
107
- if fmt not in ("json", "pytest", "playwright") or not isinstance(tests, list):
108
- raise ValueError("check_collection_invalid_format")
109
- for test in tests:
110
- if not isinstance(test, Mapping) or not isinstance(test.get("id"), str):
111
- raise ValueError("check_collection_invalid_tests")
112
- for field in ("capabilities", "markers"):
113
- values = test.get(field, [])
114
- if not isinstance(values, list) or not all(isinstance(value, str) for value in values):
115
- raise ValueError("check_collection_invalid_tests")
116
- return tests
117
-
118
-
119
- def _finding(code: str, scenario: str, check: str, message: str) -> dict[str, str]:
120
- return {"code": code, "scenario": scenario, "check_id": check, "message": message}
121
-
122
-
123
- def _execution_argv(root: str, check: Mapping[str, object]) -> list[str]:
124
- command = check.get("command")
125
- if not isinstance(command, list) or not all(isinstance(value, str) for value in command):
126
- return []
127
- expanded = list(command)
128
- for _ in range(4):
129
- if not expanded or Path(expanded[0]).name not in ("npm", "pnpm", "yarn"):
130
- return expanded
131
- offset = 2 if len(expanded) > 1 and expanded[1] == "run" else 1
132
- if len(expanded) <= offset:
133
- return []
134
- package = Path(root) / str(check.get("cwd", ".")) / "package.json"
135
- data = json.loads(package.read_text(encoding="utf-8"))
136
- script = data.get("scripts", {}).get(expanded[offset])
137
- if not isinstance(script, str):
138
- return []
139
- lexer = shlex.shlex(script, posix=True, punctuation_chars=True)
140
- lexer.whitespace_split = True
141
- tokens = list(lexer)
142
- if any(value in ("&&", "||", ";", "|", ">", ">>", "&") for value in tokens):
143
- raise ValueError("check_execution_script_requires_direct_command")
144
- expanded = tokens + [value for value in expanded[offset + 1:] if value != "--"]
145
- return []
146
-
147
-
148
- def _execution_matches(root: str, check: Mapping[str, object], selected: Sequence[Mapping[str, object]]) -> bool:
149
- from codeflow.acceptance.cf_check_inputs import match_path
150
- argv = _execution_argv(root, check)
151
- if not argv or not selected or any(value in argv for value in ("-k", "-g", "--grep", "--grep-invert")):
152
- return False
153
- pytest = any(Path(value).name in ("pytest", "py.test") for value in argv)
154
- markers = _pytest_markers(argv) if pytest else []
155
- if markers and any(not set(markers) <= set(item.get("markers", [])) for item in selected):
156
- return False
157
- selectors: list[str] = []
158
- for value in argv:
159
- normalized = value.replace("\\", "/").removeprefix("./")
160
- if normalized.startswith(str(Path(root).resolve()) + "/"):
161
- normalized = normalized[len(str(Path(root).resolve())) + 1:]
162
- if re.fullmatch(r"[\w./*?:-]+\.(?:py|[cm]?[jt]sx?)(?::[\w:\[\].-]+)?", normalized):
163
- selectors.append(normalized)
164
- if selectors:
165
- return all(any(match_path(str(item["id"]).split("@", 1)[0], selector)
166
- or match_path(str(item["id"]).split("@", 1)[0].split(":", 1)[0], selector)
167
- for selector in selectors) for item in selected)
168
- if pytest:
169
- return bool(markers)
170
- return any(Path(value).name in ("playwright", "cypress") for value in argv)
171
-
172
-
173
- def _collected(root: str, check: Mapping[str, object], deadline: float | None = None) -> dict[str, Mapping[str, object]]:
174
- config = check.get("collect")
175
- if not isinstance(config, Mapping):
176
- raise ValueError("check_collection_missing")
177
- argv = config.get("argv")
178
- if not isinstance(argv, list) or not argv or not all(isinstance(item, str) for item in argv):
179
- raise ValueError("check_collection_invalid")
180
- timeout = config.get("timeout", 10)
181
- if isinstance(timeout, bool) or not isinstance(timeout, (float, int)) or not 0 < timeout <= 60:
182
- raise ValueError("check_collection_timeout_invalid")
183
- directory = (Path(root) / str(check.get("cwd", "."))).resolve()
184
- if directory != Path(root).resolve() and Path(root).resolve() not in directory.parents:
185
- raise ValueError("check_collection_outside_project")
186
- result = run_command(argv, str(directory), float(timeout), deadline)
187
- if result.get("status") != "ok" or result.get("returncode") != 0:
188
- raise CollectionError("check_collection_failed", {"command": argv, "cwd": str(directory),
189
- "exit_code": result.get("returncode"), "status": result.get("status"),
190
- "output": (str(result.get("stdout", "")) + str(result.get("stderr", "")))[-4000:]})
191
- output = str(result.get("stdout", ""))
192
- if len(output.encode()) > 2_000_000:
193
- raise ValueError("check_collection_too_large")
194
- tests = _parse_collection(output, config, root)
195
- if len({item["id"] for item in tests}) != len(tests):
196
- raise ValueError("check_collection_duplicate_test")
197
- return {str(item["id"]): item for item in tests}
198
-
199
-
200
- def _requirements(row: Mapping[str, object]) -> tuple[bool, set[str]]:
201
- browser = str(row.get("level", row.get("kind", ""))).lower() == "e2e"
202
- markers: set[str] = set()
203
- if row.get("smoke"):
204
- markers.add("smoke")
205
- if row.get("level") == "capacity" or "capacity" in row.get("tags", []):
206
- markers.add("capacity")
207
- return browser, markers
208
-
209
-
210
- def registration_required(row: Mapping[str, object]) -> bool:
211
- """Return whether a scenario needs collection-backed registration."""
212
- browser, markers = _requirements(row)
213
- return bool(browser or markers)
214
-
215
-
216
- def _collection_config(root: str, check: Mapping[str, object], markers: set[str]) -> dict[str, object]:
217
- argv = _execution_argv(root, check)
218
- if any(Path(value).name in ("pytest", "py.test") for value in argv):
219
- if "-k" in argv:
220
- raise ValueError("check_execution_selection_mismatch: use concrete pytest node IDs")
221
- selected_markers = set(_pytest_markers(argv)) | markers
222
- argv = [value for value in argv if not re.fullmatch(r"-q+|--quiet", value)]
223
- argv += ["--collect-only", "--verbosity=-1"]
224
- if selected_markers:
225
- argv += ["-m", " and ".join(sorted(selected_markers))]
226
- return {"argv": argv, "format": "pytest", "timeout": 10}
227
- if any(Path(value).name == "playwright" for value in argv):
228
- return {"argv": argv + ["--list", "--reporter=json"], "format": "playwright", "timeout": 10}
229
- raise ValueError("check_collection_inference_unsupported: supply --registration-file")
230
-
231
-
232
- def register_check(root: str, check: dict[str, object], rows: Sequence[Mapping[str, object]],
233
- deadline: float) -> list[dict[str, str]]:
234
- """Collect, populate and validate one check against all of its scenarios."""
235
- validate_checks([check])
236
- effective_rows = [{**row, "level": check.get("level", row.get("level"))} for row in rows]
237
- requirements = [_requirements(row) for row in effective_rows]
238
- browser = any(item[0] for item in requirements)
239
- markers = set().union(*(item[1] for item in requirements))
240
- if "collect" not in check:
241
- check["collect"] = _collection_config(root, check, markers | ({"browser"} if browser else set()))
242
- collected = _collected(root, check, deadline)
243
- if not collected:
244
- raise ValueError("check_collection_empty")
245
- check.setdefault("test_ids", sorted(collected))
246
- selected = [collected[value] for value in check["test_ids"] if value in collected]
247
- actual_markers = set.intersection(*(set(item.get("markers", [])) for item in selected)) if selected else set()
248
- check.setdefault("markers", sorted(actual_markers))
249
- if browser and selected and all("browser" in item.get("capabilities", []) for item in selected):
250
- check.setdefault("suite", "browser")
251
- validate_checks([check])
252
- return [issue for row in effective_rows for issue in _registration_issues(root, row, check, collected)]
253
-
254
-
255
- def _registration_issues(root: str, row: Mapping[str, object], check: Mapping[str, object],
256
- collected: Mapping[str, Mapping[str, object]]) -> list[dict[str, str]]:
257
- scenario, check_id = str(row.get("id", "")), str(check.get("id", ""))
258
- browser, markers = _requirements(row)
259
- errors: list[dict[str, str]] = []
260
- test_ids = check.get("test_ids", [])
261
- if not isinstance(test_ids, list) or not test_ids or not all(isinstance(value, str) for value in test_ids):
262
- return [_finding("check_test_ids_missing", scenario, check_id, "check 必须登记具体测试 ID")]
263
- selected = [collected[value] for value in test_ids if value in collected]
264
- if len(selected) != len(test_ids):
265
- errors.append(_finding("check_test_not_collected", scenario, check_id, "登记的测试 ID 未全部被真实收集"))
266
- try:
267
- matches = _execution_matches(root, check, selected)
268
- except (OSError, ValueError) as exc:
269
- errors.append(_finding("check_execution_config_invalid", scenario, check_id, str(exc)))
270
- matches = False
271
- if not matches:
272
- errors.append(_finding("check_execution_selection_mismatch", scenario, check_id,
273
- "实际执行命令必须选择登记用例;不可验证的 grep/-k 过滤请改用具体测试 ID"))
274
- if browser and (check.get("suite") != "browser" or any("browser" not in item.get("capabilities", []) for item in selected)):
275
- errors.append(_finding("e2e_check_not_browser", scenario, check_id, "E2E check 必须属于 browser 套件并收集到浏览器能力用例"))
276
- if check.get("level") and check.get("level") != "e2e" and any(
277
- "browser" in item.get("capabilities", []) for item in selected
278
- ):
279
- errors.append(_finding("check_level_browser_mismatch", scenario, check_id, "浏览器用例必须声明 e2e 层级"))
280
- for marker in sorted(markers):
281
- if marker not in check.get("markers", []) or any(marker not in item.get("markers", []) for item in selected):
282
- code = "capacity_check_not_marked" if marker == "capacity" else "smoke_check_not_registered"
283
- errors.append(_finding(code, scenario, check_id, f"check 与真实收集用例必须包含 {marker} 标记"))
284
- return errors
285
-
286
-
287
- def validate_registration(root: str, scenarios: Sequence[Mapping[str, object]],
288
- checks: Sequence[Mapping[str, object]]) -> dict[str, object]:
289
- by_id = {str(check["id"]): check for check in checks}
290
- cache: dict[str, dict[str, Mapping[str, object]] | Exception] = {}
291
- errors: list[dict[str, object]] = []
292
- deadline = time.monotonic() + 60
293
- runnable_smoke = runnable_browser = 0
294
- for row in scenarios:
295
- browser, markers = _requirements(row)
296
- ids = scenario_check_ids(row)
297
- has_browser_check = any(by_id.get(check_id, {}).get("level") == "e2e" for check_id in ids)
298
- if not browser and not markers and not has_browser_check:
299
- continue
300
- row_errors: list[dict[str, object]] = []
301
- if not ids:
302
- row_errors.append(_finding("scenario_check_missing", str(row["id"]), "", "需要可执行 check 绑定"))
303
- for check_id in ids:
304
- check = by_id.get(check_id)
305
- if check is None:
306
- row_errors.append(_finding("unknown_check", str(row["id"]), check_id, "引用的 check 不存在"))
307
- continue
308
- effective = {**row, "level": check.get("level", row.get("level"))}
309
- if not registration_required(effective):
310
- continue
311
- if check_id not in cache:
312
- try:
313
- cache[check_id] = _collected(root, check, deadline)
314
- except (OSError, ValueError) as exc:
315
- cache[check_id] = exc
316
- collected = cache[check_id]
317
- if isinstance(collected, Exception):
318
- detail = collected.details if isinstance(collected, CollectionError) else {}
319
- row_errors.append({**_finding(str(collected), str(row["id"]), check_id, "无法验证测试收集与套件登记"), **detail})
320
- else:
321
- row_errors.extend(_registration_issues(root, effective, check, collected))
322
- errors.extend(row_errors)
323
- runnable_smoke += int(bool(row.get("smoke")) and not row_errors)
324
- runnable_browser += int(browser and not row_errors)
325
- return {"ok": not errors, "errors": errors, "runnable_smoke_count": runnable_smoke,
326
- "runnable_browser_count": runnable_browser, "collected_checks": len(cache)}
@@ -1,333 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Spec frontmatter checks: parse, cache and execute (quality loop FEAT-01).
3
-
4
- Contract (spec-quality-loop.design.md §2.3.2 / §3.4 / §3.5):
5
- - Checks live in spec frontmatter `checks:` so rule text and machine check
6
- stay in the same file (ADR: frontmatter over checks.yml).
7
- - Invalid entries are skipped and surfaced as parse errors (E-01) — they must
8
- never break parsing of sibling checks or spec injection.
9
- - Execution guards: per-check timeout (E-02), oversized content skipped
10
- (B-01), disabled checks filtered (RULE-04 state), matched lines per check
11
- capped so feedback stays small (RISK-05).
12
- - yaml import is lazy: specs without `checks:` never pay for it on the
13
- PostToolUse hot path (NFR-PERF-01).
14
- """
15
- import fnmatch
16
- import os
17
- import re
18
- from typing import Mapping
19
- from concurrent.futures import ThreadPoolExecutor
20
- from concurrent.futures import TimeoutError as FutureTimeout
21
-
22
- from codeflow.core import cf_log
23
- from codeflow.core.cf_core import normalize_path
24
- from codeflow.core.cf_markdown import frontmatter_block
25
-
26
- CHECK_TIMEOUT_SECONDS = 2.0
27
- MAX_CONTENT_BYTES = 256 * 1024
28
- MESSAGE_MAX_LEN = 200
29
- MAX_LINES_PER_CHECK = 3
30
-
31
- _ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
32
- IMPLEMENTED_TYPES = ("regex",)
33
- RESERVED_TYPES = ("ast", "cmd") # interface reserved, implementation deferred
34
- _SEVERITIES = ("warn", "info") # no "error": feedback only, never blocking
35
-
36
- _checks_cache: dict = {}
37
-
38
- STATE_NAME = ".check-state.json"
39
- FP_DISABLE_COUNT = 3
40
- FP_DISABLE_RATE = 0.10
41
-
42
-
43
- def state_path(project_root: str) -> str:
44
- return os.path.join(project_root, ".code-flow", STATE_NAME)
45
-
46
-
47
- def load_check_state(project_root: str) -> dict:
48
- """Read check-state; corrupt/missing files rebuild as empty (tolerant)."""
49
- import json
50
- try:
51
- with open(state_path(project_root), "r", encoding="utf-8") as f:
52
- data = json.load(f)
53
- return data if isinstance(data, dict) else {}
54
- except Exception:
55
- return {}
56
-
57
-
58
- def save_check_state(project_root: str, state: dict) -> bool:
59
- import json
60
- try:
61
- path = state_path(project_root)
62
- os.makedirs(os.path.dirname(path), exist_ok=True)
63
- with open(path, "w", encoding="utf-8") as f:
64
- json.dump(state, f, ensure_ascii=False)
65
- return True
66
- except Exception as exc:
67
- cf_log.warn(f"save_check_state failed: {exc}")
68
- return False
69
-
70
-
71
- def record_hits(project_root: str, check_ids: list) -> dict:
72
- """Increment hit_count for triggered checks (误报率分母). Returns state."""
73
- state = load_check_state(project_root)
74
- for cid in check_ids:
75
- entry = state.setdefault(cid, {})
76
- entry["hit_count"] = int(entry.get("hit_count", 0)) + 1
77
- save_check_state(project_root, state)
78
- return state
79
-
80
-
81
- def record_false_positive(project_root: str, check_id: str) -> dict:
82
- """Mark one false positive; auto-disable past RULE-04 thresholds.
83
-
84
- Returns {"check_id", "fp_count", "hit_count", "disabled"}.
85
- """
86
- state = load_check_state(project_root)
87
- entry = state.setdefault(check_id, {})
88
- entry["fp_count"] = int(entry.get("fp_count", 0)) + 1
89
- fp = entry["fp_count"]
90
- hits = int(entry.get("hit_count", 0))
91
- if not entry.get("disabled") and (
92
- fp >= FP_DISABLE_COUNT or (hits > 0 and fp / hits > FP_DISABLE_RATE)
93
- ):
94
- entry["disabled"] = True
95
- entry["disabled_reason"] = (
96
- f"auto: fp_count={fp}" if fp >= FP_DISABLE_COUNT
97
- else f"auto: fp_rate={fp}/{hits}"
98
- )
99
- save_check_state(project_root, state)
100
- return {
101
- "check_id": check_id,
102
- "fp_count": fp,
103
- "hit_count": hits,
104
- "disabled": bool(entry.get("disabled")),
105
- }
106
-
107
-
108
- def _validate_entry(entry: object, index: int, seen_ids: set) -> tuple:
109
- """Validate one raw checks entry → (check_dict | None, error | None)."""
110
- if not isinstance(entry, dict):
111
- return None, f"checks[{index}]: 不是对象,已跳过"
112
- cid = entry.get("id")
113
- if not isinstance(cid, str) or not _ID_RE.match(cid):
114
- return None, f"checks[{index}]: id 缺失或非 kebab-case,已跳过"
115
- if cid in seen_ids:
116
- return None, f"checks[{index}]: id '{cid}' 重复,已跳过"
117
- ctype = entry.get("type")
118
- if ctype in RESERVED_TYPES:
119
- return None, f"{cid}: type '{ctype}' 暂未实现(接口预留),已跳过"
120
- if ctype not in IMPLEMENTED_TYPES:
121
- return None, f"{cid}: 未知 type '{ctype}',已跳过"
122
- pattern = entry.get("pattern")
123
- if not isinstance(pattern, str) or not pattern:
124
- return None, f"{cid}: pattern 缺失,已跳过"
125
- try:
126
- compiled = re.compile(pattern)
127
- except re.error as exc:
128
- return None, f"{cid}: pattern 非法正则({exc}),已跳过"
129
-
130
- message = entry.get("message")
131
- if not isinstance(message, str) or not message.strip():
132
- return None, f"{cid}: message 缺失,已跳过"
133
- error = None
134
- if len(message) > MESSAGE_MAX_LEN:
135
- error = f"{cid}: message 超过 {MESSAGE_MAX_LEN} 字符,运行时截断"
136
- message = message[:MESSAGE_MAX_LEN]
137
-
138
- severity = entry.get("severity", "warn")
139
- if severity not in _SEVERITIES:
140
- error = error or f"{cid}: severity '{severity}' 非法,按 warn 处理"
141
- severity = "warn"
142
-
143
- # default "*": fnmatch's "*" crosses "/" so it matches any depth;
144
- # "**/*" under fnmatch would demand at least one directory level and
145
- # silently skip repo-root files (same pitfall fixed in config patterns)
146
- files = entry.get("files")
147
- if not isinstance(files, str) or not files:
148
- files = "*"
149
-
150
- return {
151
- "id": cid,
152
- "type": ctype,
153
- "pattern": pattern,
154
- "regex": compiled,
155
- "files": files,
156
- "message": message,
157
- "severity": severity,
158
- }, error
159
-
160
-
161
- def parse_spec_checks(content: str) -> tuple:
162
- """Parse frontmatter checks → (checks, errors). Both lists, never raises."""
163
- if not isinstance(content, str):
164
- return [], []
165
- match = frontmatter_block(content)
166
- if match is None or "checks:" not in match:
167
- return [], []
168
- try:
169
- import yaml
170
- meta = yaml.safe_load(match) or {}
171
- except Exception as exc:
172
- return [], [f"frontmatter YAML 解析失败: {exc}"]
173
- raw = meta.get("checks")
174
- if raw is None:
175
- return [], []
176
- if not isinstance(raw, list):
177
- return [], ["checks 必须是列表"]
178
- checks: list = []
179
- errors: list = []
180
- seen_ids: set = set()
181
- for index, entry in enumerate(raw):
182
- check, error = _validate_entry(entry, index, seen_ids)
183
- if error:
184
- errors.append(error)
185
- if check:
186
- checks.append(check)
187
- seen_ids.add(check["id"])
188
- return checks, errors
189
-
190
-
191
- def load_spec_checks(spec_abs_path: str) -> tuple:
192
- """Parse a spec file's checks with mtime cache → (checks, errors)."""
193
- try:
194
- mtime = os.path.getmtime(spec_abs_path)
195
- except OSError:
196
- return [], []
197
- cached = _checks_cache.get(spec_abs_path)
198
- if cached and cached["mtime"] == mtime:
199
- return cached["checks"], cached["errors"]
200
- try:
201
- with open(spec_abs_path, "r", encoding="utf-8") as f:
202
- checks, errors = parse_spec_checks(f.read())
203
- except Exception as exc:
204
- cf_log.warn(f"load_spec_checks failed {spec_abs_path}: {exc}")
205
- return [], []
206
- _checks_cache[spec_abs_path] = {
207
- "mtime": mtime, "checks": checks, "errors": errors,
208
- }
209
- return checks, errors
210
-
211
-
212
- # 纠正句式(FEAT-04 信号①):保守收录、宁漏勿误(E-06)。
213
- # 否定排除:不要紧/不要紧张、对不对、不对外/不对称。
214
- _CORRECTION_PATTERNS = [
215
- r"不要(?!紧)",
216
- r"(?<!对)不对(?![外称])",
217
- r"不是这样",
218
- r"我说过",
219
- r"改回[去来]",
220
- r"别再?用",
221
- r"写错了",
222
- r"\bdon'?t\s+use\b",
223
- r"\bthat'?s\s+wrong\b",
224
- r"\bI\s+said\b",
225
- r"\brevert\s+(that|this|it)\b",
226
- r"\bstop\s+using\b",
227
- ]
228
- _CORRECTION_RE = re.compile("|".join(_CORRECTION_PATTERNS), re.IGNORECASE)
229
-
230
-
231
- def detect_correction(prompt: str) -> dict:
232
- """Detect a correction phrase in a user prompt → {"phrase", "span"} | None.
233
-
234
- Single combined regex, first hit wins; the wordlist is deliberately
235
- conservative — a missed correction only delays learning, a false hit
236
- pollutes the candidate stream.
237
- """
238
- if not isinstance(prompt, str) or not prompt:
239
- return None
240
- match = _CORRECTION_RE.search(prompt)
241
- if not match:
242
- return None
243
- return {"phrase": match.group(0), "span": [match.start(), match.end()]}
244
-
245
-
246
- def _match_lines(regex, content: str) -> list:
247
- hits = []
248
- for line_no, line in enumerate(content.splitlines(), 1):
249
- if regex.search(line):
250
- hits.append((line_no, line.strip()))
251
- if len(hits) >= MAX_LINES_PER_CHECK:
252
- break
253
- return hits
254
-
255
-
256
- def run_checks(
257
- checks: list,
258
- rel_path: str,
259
- content: str,
260
- state: dict = None,
261
- timeout: float = CHECK_TIMEOUT_SECONDS,
262
- ) -> tuple:
263
- """Execute checks against one file's content → (violations, skipped).
264
-
265
- violations: [{check_id, file, line_no, line, message, severity}]
266
- skipped: [{check_id, reason}] — timeout / oversize, caller records
267
- degrade events (E-02 / B-01).
268
- Disabled checks (state[check_id]["disabled"]) are silently filtered.
269
- """
270
- violations: list = []
271
- skipped: list = []
272
- if not checks or not isinstance(content, str):
273
- return violations, skipped
274
- if len(content.encode("utf-8", errors="ignore")) > MAX_CONTENT_BYTES:
275
- return violations, [{"check_id": "*", "reason": "content_too_large"}]
276
-
277
- rel_path = normalize_path(rel_path)
278
- state = state or {}
279
- with ThreadPoolExecutor(max_workers=1) as pool:
280
- for check in checks:
281
- if state.get(check["id"], {}).get("disabled"):
282
- continue
283
- if not fnmatch.fnmatch(rel_path, check["files"]):
284
- continue
285
- future = pool.submit(_match_lines, check["regex"], content)
286
- try:
287
- hits = future.result(timeout=timeout)
288
- except FutureTimeout:
289
- skipped.append({"check_id": check["id"], "reason": "timeout"})
290
- future.cancel()
291
- continue
292
- except Exception as exc:
293
- skipped.append({"check_id": check["id"], "reason": f"error:{exc}"})
294
- continue
295
- for line_no, line in hits:
296
- violations.append({
297
- "check_id": check["id"],
298
- "file": rel_path,
299
- "line_no": line_no,
300
- "line": line[:160],
301
- "message": check["message"],
302
- "severity": check["severity"],
303
- })
304
- return violations, skipped
305
-
306
-
307
- def run_regex_verifier(
308
- pattern: str,
309
- files: str,
310
- contents: Mapping[str, str],
311
- timeout: float = CHECK_TIMEOUT_SECONDS,
312
- ) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
313
- """Execute one verifier regex over explicit file contents."""
314
- try:
315
- compiled = re.compile(pattern)
316
- except re.error as exc:
317
- return [], [{"check_id": "verifier", "reason": f"invalid_regex:{exc}"}]
318
- check = {
319
- "id": "verifier",
320
- "type": "regex",
321
- "pattern": pattern,
322
- "regex": compiled,
323
- "files": files,
324
- "message": "Rule verifier matched forbidden content",
325
- "severity": "warn",
326
- }
327
- violations: list[dict[str, object]] = []
328
- skipped: list[dict[str, object]] = []
329
- for relative, content in contents.items():
330
- file_violations, file_skipped = run_checks([check], relative, content, timeout=timeout)
331
- violations.extend(file_violations)
332
- skipped.extend(file_skipped)
333
- return violations, skipped