@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,60 +0,0 @@
1
- #!/usr/bin/env python3
2
- """CLI wrapper for the fail-closed Spec Context Stage Gate."""
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import os
8
- from pathlib import Path
9
- import sys
10
- import time
11
- from typing import IO, Optional, Sequence
12
-
13
- from codeflow.core.cf_core import phase_timing
14
- from codeflow.spec.cf_spec_context_model import load_context
15
- from codeflow.spec.cf_stage_gate import (
16
- GateIssue, GateResult, evidence_deferral_kind, result_to_data, validate_plan_coverage, validate_stage,
17
- )
18
-
19
-
20
- def _merge_results(first: GateResult, second: GateResult) -> GateResult:
21
- errors = (*first.errors, *second.errors)
22
- warnings = (*first.warnings, *second.warnings)
23
- refs = tuple(sorted(set((*first.affected_refs, *second.affected_refs))))
24
- return GateResult("block" if errors else "pass", errors, warnings, refs)
25
-
26
-
27
- def _parser() -> argparse.ArgumentParser:
28
- parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND", "cf_spec_gate.py"))
29
- parser.add_argument("--task-dir", required=True)
30
- parser.add_argument("--stage", required=True)
31
- parser.add_argument("--task", default="")
32
- parser.add_argument("--artifact", default="")
33
- parser.add_argument("--json", action="store_true")
34
- return parser
35
-
36
-
37
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
38
- started = time.monotonic()
39
- try:
40
- args = _parser().parse_args(argv)
41
- phase_started = time.monotonic()
42
- context = load_context(str(Path(args.task_dir) / "spec-context.yml"))
43
- phase_timing("gate.load_context", phase_started)
44
- phase_started = time.monotonic()
45
- directory = Path(args.task_dir).resolve()
46
- root = next((parent.parent for parent in (directory, *directory.parents)
47
- if parent.name == ".code-flow"), directory)
48
- result = validate_stage(context, args.stage, task_id=args.task, root=str(root), task_dir=args.task_dir)
49
- phase_timing("gate.validate_stage", phase_started)
50
- if args.stage == "plan" and args.artifact:
51
- phase_started = time.monotonic()
52
- result = _merge_results(result, validate_plan_coverage(context, args.artifact))
53
- phase_timing("gate.validate_plan", phase_started)
54
- stdout.write(json.dumps(result_to_data(result), ensure_ascii=False))
55
- phase_timing("gate.total", started)
56
- return 0 if result.decision == "pass" else 3
57
- except Exception as exc:
58
- sys.stderr.write(f"cf_spec_gate error: {exc}\n")
59
- stdout.write(json.dumps({"decision": "block", "errors": [{"code": "gate_error"}]}, ensure_ascii=False))
60
- return 2
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env python3
2
- """CLI wrapper for TASK-bound Spec Context projection."""
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import os
8
- from pathlib import Path
9
- import sys
10
- from typing import IO, Optional, Sequence
11
-
12
- from codeflow.spec.cf_spec_context_model import context_sha256, load_context
13
- from codeflow.spec.cf_task_projection import SessionProjection, project_task_session
14
- from codeflow.task.cf_task_doc import task_section, task_spec_refs
15
-
16
-
17
- def _task_section(text: str, task_id: str) -> str:
18
- return task_section(text, task_id).body
19
-
20
-
21
- def _refs(section: str) -> tuple[str, ...]:
22
- return task_spec_refs(section)
23
-
24
-
25
- def _parser() -> argparse.ArgumentParser:
26
- parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND", "cf_spec_session.py"))
27
- parser.add_argument("--task-dir", required=True)
28
- parser.add_argument("--task-file", required=True)
29
- parser.add_argument("--task", required=True)
30
- parser.add_argument("--output", required=True)
31
- parser.add_argument("--budget", type=int, default=12000)
32
- parser.add_argument("--json", action="store_true")
33
- return parser
34
-
35
-
36
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
37
- try:
38
- args = _parser().parse_args(argv)
39
- context = load_context(str(Path(args.task_dir) / "spec-context.yml"))
40
- result = project_task_session(context, args.task_file, args.task, args.budget)
41
- Path(args.output).write_text(result.text, encoding="utf-8")
42
- stdout.write(json.dumps(result.__dict__, ensure_ascii=False))
43
- return 0
44
- except Exception as exc:
45
- stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
46
- return 3
@@ -1,540 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import argparse
4
- import os
5
- import sys
6
- from pathlib import Path
7
- from typing import Optional
8
-
9
- from codeflow.core import cf_log
10
- from codeflow.acceptance.cf_checks import load_check_state
11
- from codeflow.core.cf_core import (
12
- build_effective_mapping,
13
- build_spec_catalog,
14
- compress_content,
15
- estimate_tokens,
16
- load_config,
17
- project_instruction_file,
18
- non_injectable_specs,
19
- resolve_quality_loop,
20
- )
21
-
22
-
23
- def _violation_fixed(index: int, events: list) -> bool:
24
- """修正口径:违规后同会话同文件有后续编辑,且其后无同 check 再违规。
25
-
26
- 用日志追加顺序判先后(ts 仅秒级精度,同秒事件无法靠时间戳排序)。
27
- 保留单点查询语义;批量聚合请用 violation_fixed_batch(一次线性扫描)。
28
- """
29
- violation = events[index]
30
- v_data = violation.get("data") or {}
31
- later_edit = False
32
- for event in events[index + 1:]:
33
- if event.get("sid") != violation.get("sid"):
34
- continue
35
- data = event.get("data") or {}
36
- if event.get("event") == "edit" and data.get("file") == v_data.get("file"):
37
- later_edit = True
38
- if (
39
- later_edit
40
- and event.get("event") == "violation"
41
- and data.get("check_id") == v_data.get("check_id")
42
- and data.get("file") == v_data.get("file")
43
- ):
44
- return False
45
- return later_edit
46
-
47
-
48
- def violation_fixed_batch(events: list) -> list[bool]:
49
- """Batch twin of _violation_fixed: one reverse pass, identical semantics.
50
-
51
- Segments the timeline per (session, file) at edits: a violation is fixed
52
- iff an edit exists after it and no same-check violation exists in a later
53
- segment. No slicing, no per-violation rescan: O(n) time.
54
- """
55
- fixed: dict[int, bool] = {}
56
- edit_right: set[tuple[str, str]] = set()
57
- current: dict[tuple[str, str], set[str]] = {}
58
- later: dict[tuple[str, str], set[str]] = {}
59
- for index in range(len(events) - 1, -1, -1):
60
- event = events[index]
61
- if not isinstance(event, dict):
62
- continue
63
- kind = event.get("event")
64
- data = event.get("data")
65
- if not isinstance(data, dict):
66
- data = {}
67
- key = (str(event.get("sid")), str(data.get("file")))
68
- if kind == "edit":
69
- later.setdefault(key, set()).update(current.get(key, ()))
70
- current[key] = set()
71
- edit_right.add(key)
72
- elif kind == "violation":
73
- check = str(data.get("check_id"))
74
- fixed[index] = key in edit_right and check not in later.get(key, ())
75
- current.setdefault(key, set()).add(check)
76
- return [fixed[index] for index, event in enumerate(events) if isinstance(event, dict) and event.get("event") == "violation"]
77
-
78
-
79
- def quality_loop_summary(project_root: str, config: dict) -> dict:
80
- """FEAT-05/02 度量聚合:Top 违规榜 / 修正率 / 误报与停用 / 降级组件。"""
81
- switches = resolve_quality_loop(config)
82
- events = cf_log.read_events(project_root, days=30)
83
- summary = {"switches": switches, "window_days": 30}
84
- if not events:
85
- summary["note"] = "暂无数据"
86
- return summary
87
-
88
- violations = [e for e in events if e.get("event") == "violation"]
89
- counts: dict = {}
90
- for v in violations:
91
- data = v.get("data") or {}
92
- key = f"{data.get('spec', '?')}#{data.get('check_id', '?')}"
93
- counts[key] = counts.get(key, 0) + 1
94
- summary["top_violations"] = sorted(
95
- ({"rule": k, "count": n} for k, n in counts.items()),
96
- key=lambda item: -item["count"],
97
- )[:10]
98
-
99
- fixed = violation_fixed_batch(events)
100
- summary["violation_total"] = len(violations)
101
- summary["fix_rate"] = (
102
- f"{round(sum(fixed) * 100 / len(violations))}%" if violations else "n/a"
103
- )
104
-
105
- state = load_check_state(project_root)
106
- summary["checks"] = {
107
- cid: {
108
- "hit_count": entry.get("hit_count", 0),
109
- "fp_count": entry.get("fp_count", 0),
110
- "disabled": bool(entry.get("disabled")),
111
- "disabled_reason": entry.get("disabled_reason", ""),
112
- }
113
- for cid, entry in state.items()
114
- if isinstance(entry, dict) and not cid.startswith("_")
115
- }
116
-
117
- degraded: dict = {}
118
- for event in events:
119
- if event.get("event") != "degrade":
120
- continue
121
- data = event.get("data") or {}
122
- component = data.get("component", "?")
123
- item = degraded.setdefault(component, {"count": 0, "last_error": ""})
124
- item["count"] += 1
125
- item["last_error"] = data.get("error", "")
126
- summary["degraded"] = degraded
127
- return summary
128
-
129
-
130
- def _percent(numerator: int, denominator: int) -> str:
131
- return f"{round(numerator * 100 / denominator)}%" if denominator else "n/a"
132
-
133
-
134
- def spec_workflow_summary(project_root: str) -> dict:
135
- events = cf_log.read_events(project_root, days=30)
136
- candidates = {
137
- (str((event.get("data") or {}).get("task")), str((event.get("data") or {}).get("rule")))
138
- for event in events if event.get("event") == "spec_candidate"
139
- }
140
- bound = {
141
- (str((event.get("data") or {}).get("task")), str((event.get("data") or {}).get("rule")))
142
- for event in events if event.get("event") == "spec_bound"
143
- }
144
- gates = [event for event in events if event.get("event") == "spec_gate"]
145
- first_pass = sum((event.get("data") or {}).get("first_pass") is True for event in gates)
146
- late = sum(event.get("event") == "late_violation" for event in events)
147
- statuses = {"stale": 0, "conflict": 0, "not_applicable": 0}
148
- for event in events:
149
- status = (event.get("data") or {}).get("status")
150
- if status in statuses:
151
- statuses[status] += 1
152
- degraded = sum(event.get("event") == "metrics_degraded" for event in events)
153
- return {
154
- "window_days": 30,
155
- "coverage": _percent(len(bound & candidates), len(candidates)),
156
- "first_compliance_rate": _percent(first_pass, len(gates)),
157
- "late_violation_rate": _percent(late, len(bound)),
158
- "statuses": statuses,
159
- "data_complete": degraded == 0,
160
- "degraded_events": degraded,
161
- "read_note": "read 仅表示读取,不代表理解",
162
- }
163
-
164
-
165
- def read_text(path: str) -> str:
166
- try:
167
- with open(path, "r", encoding="utf-8") as file:
168
- return file.read().strip()
169
- except Exception:
170
- return ""
171
-
172
-
173
- def normalize_rel_path(path: str) -> str:
174
- return path.replace(os.sep, "/")
175
-
176
-
177
- def extract_spec_path(spec_entry) -> str:
178
- if isinstance(spec_entry, dict):
179
- return normalize_rel_path(spec_entry.get("path", ""))
180
- if isinstance(spec_entry, str):
181
- return normalize_rel_path(spec_entry)
182
- return ""
183
-
184
-
185
- def discover_specs(specs_root: str) -> dict:
186
- discovered = {}
187
- if not os.path.isdir(specs_root):
188
- return discovered
189
-
190
- for root, _, files in os.walk(specs_root):
191
- for filename in files:
192
- if not filename.endswith(".md"):
193
- continue
194
- full_path = os.path.join(root, filename)
195
- rel = normalize_rel_path(os.path.relpath(full_path, specs_root))
196
- parts = rel.split("/", 1)
197
- if len(parts) < 2:
198
- continue
199
- domain = parts[0]
200
- discovered.setdefault(domain, []).append(rel)
201
-
202
- for domain in discovered:
203
- discovered[domain] = sorted(set(discovered[domain]))
204
- return discovered
205
-
206
-
207
- def configured_specs(config: dict, domain: str) -> list:
208
- mapping = (config.get("path_mapping") or {}).get(domain) or {}
209
- specs_config = mapping.get("specs") or []
210
- result = []
211
- for spec_entry in specs_config:
212
- rel = extract_spec_path(spec_entry)
213
- if rel:
214
- result.append(rel)
215
- return result
216
-
217
-
218
- def resolve_domains(config: dict, discovered: dict, domain_filter: Optional[str]) -> list:
219
- if domain_filter:
220
- if domain_filter in discovered:
221
- return [domain_filter]
222
- if domain_filter in (config.get("path_mapping") or {}):
223
- return [domain_filter]
224
- return []
225
-
226
- if discovered:
227
- return sorted(discovered.keys())
228
- return sorted((config.get("path_mapping") or {}).keys())
229
-
230
-
231
- def _build_item(rel: str, raw_content: str) -> dict:
232
- raw_tokens = estimate_tokens(raw_content)
233
- compressed_tokens = estimate_tokens(compress_content(raw_content))
234
- saved_pct = (
235
- round((raw_tokens - compressed_tokens) * 100 / raw_tokens, 1)
236
- if raw_tokens
237
- else 0.0
238
- )
239
- return {
240
- "path": rel,
241
- "tokens": compressed_tokens,
242
- "tokens_raw": raw_tokens,
243
- "tokens_compressed": compressed_tokens,
244
- "saved_pct": saved_pct,
245
- }
246
-
247
-
248
- def collect_domain_items(
249
- specs_root: str,
250
- domain: str,
251
- configured: list,
252
- discovered: list,
253
- ) -> tuple:
254
- items = []
255
- missing = []
256
- seen = set()
257
-
258
- for rel in configured:
259
- if rel in seen:
260
- continue
261
- seen.add(rel)
262
- full_path = os.path.join(specs_root, rel)
263
- if not os.path.exists(full_path):
264
- missing.append({"domain": domain, "path": rel})
265
- continue
266
- content = read_text(full_path)
267
- if content:
268
- items.append(_build_item(rel, content))
269
-
270
- for rel in discovered:
271
- if rel in seen:
272
- continue
273
- seen.add(rel)
274
- full_path = os.path.join(specs_root, rel)
275
- content = read_text(full_path)
276
- if content:
277
- items.append(_build_item(rel, content))
278
-
279
- return items, missing
280
-
281
-
282
- def _arguments() -> argparse.Namespace:
283
- parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND", "cf_stats.py"))
284
- formats = parser.add_mutually_exclusive_group()
285
- formats.add_argument("--human", action="store_true")
286
- formats.add_argument("--json", action="store_true")
287
- parser.add_argument("--audit", action="store_true")
288
- parser.add_argument("--domain", default=None)
289
- parser.add_argument("--platform", choices=("claude", "codex", "costrict", "opencode"), default="")
290
- return parser.parse_args()
291
-
292
-
293
- def _budget_number(config: dict[str, object], key: str, default: int) -> int:
294
- try:
295
- return int(config.get(key, default))
296
- except (TypeError, ValueError) as exc:
297
- sys.stderr.write(f"cf-stats invalid {key}: {exc}; using {default}\n")
298
- return default
299
-
300
-
301
- def _domain_usage(root: str, config: dict[str, object], domain_filter: Optional[str]) -> dict[str, object]:
302
- specs_root = os.path.join(root, ".code-flow/specs")
303
- discovered = discover_specs(specs_root)
304
- mapping = build_effective_mapping(root, config.get("path_mapping") or {})
305
- excluded = non_injectable_specs(mapping)
306
- l1, spec_domains, missing_specs, empty_domains = {}, {}, [], []
307
- for domain in resolve_domains(config, discovered, domain_filter):
308
- configured = configured_specs(config, domain)
309
- discovered_paths = discovered.get(domain, [])
310
- items, missing = collect_domain_items(specs_root, domain, configured, discovered_paths)
311
- for relative in configured + discovered_paths:
312
- if relative:
313
- spec_domains[relative] = domain
314
- missing_specs.extend(missing)
315
- for item in items:
316
- item["injectable"] = item["path"] not in excluded
317
- if items:
318
- l1[domain] = items
319
- elif configured and (not discovered or discovered_paths):
320
- empty_domains.append(domain)
321
- tokens = sum(item["tokens"] for items in l1.values() for item in items if item["injectable"])
322
- templates = sum(item["tokens"] for items in l1.values() for item in items if not item["injectable"])
323
- return {"l1": l1, "tokens": tokens, "template_tokens": templates, "excluded": sorted(excluded),
324
- "missing_specs": missing_specs, "empty_domains": empty_domains, "spec_domain_map": spec_domains}
325
-
326
-
327
- def _compression_usage(l1: dict[str, list[dict[str, object]]]) -> dict[str, object]:
328
- items = [item for values in l1.values() for item in values if item.get("injectable", True)]
329
- raw = sum(item.get("tokens_raw", item["tokens"]) for item in items)
330
- compressed = sum(item.get("tokens_compressed", item["tokens"]) for item in items)
331
- return {"total_raw": raw, "total_compressed": compressed,
332
- "total_saved_pct": round((raw - compressed) * 100 / raw, 1) if raw else 0.0}
333
-
334
-
335
- def _usage_warnings(l0_tokens: int, l0_budget: int, l1_budget: int, total: int,
336
- total_budget: int, usage: dict[str, object]) -> list[str]:
337
- warnings = []
338
- for exceeded, message in ((l0_tokens > l0_budget, "L0 超出预算"),
339
- (total - l0_tokens > min(total_budget, l1_budget), "L1 全量聚合超过单次注入上限(不代表单次编辑超限)"),
340
- (total > total_budget, "总预算超出")):
341
- if exceeded:
342
- warnings.append(message)
343
- if usage["missing_specs"]:
344
- warnings.append(f"配置的 spec 文件缺失: {len(usage['missing_specs'])} 个")
345
- if usage["empty_domains"]:
346
- domains = ", ".join(sorted(set(usage["empty_domains"])))
347
- warnings.append(f"以下域未加载到任何 L1 spec: {domains}")
348
- return warnings
349
-
350
-
351
- def _catalog_usage(root: str, config: dict[str, object], budget: dict[str, object]) -> dict[str, object]:
352
- maximum = _budget_number(budget, "catalog_max", 200)
353
- mapping = build_effective_mapping(root, config.get("path_mapping") or {})
354
- text = build_spec_catalog(root, mapping, maximum)
355
- return {"mode": "context_first", "tokens": estimate_tokens(text), "budget": maximum,
356
- "entries": text.count("\n- `")}
357
-
358
-
359
- def _runtime_manifest_counts(manifest: Path) -> dict[str, int]:
360
- from codeflow.acceptance.cf_acceptance_risk import risk_counts
361
- from codeflow.acceptance.cf_acceptance_schema import scenario_check_ids
362
- if not manifest.is_file():
363
- return {}
364
- try:
365
- data = json.loads(manifest.read_text(encoding="utf-8"))
366
- if not isinstance(data, dict):
367
- raise ValueError("manifest must be an object")
368
- except (OSError, ValueError) as exc:
369
- sys.stderr.write(f"cf_stats: cannot read {manifest}: {exc}\n")
370
- return {}
371
- stale: set[str] = set()
372
- for row in data.get("scenarios", []):
373
- if isinstance(row, dict) and row.get("status") == "stale":
374
- ids = row.get("stale_checks") or scenario_check_ids(row) or [str(row.get("id"))]
375
- stale.update(str(value) for value in ids)
376
- return {"stale_checks": len(stale), **risk_counts(data)}
377
-
378
-
379
- def _deferred_rule_count(context: Path) -> int:
380
- from codeflow.cli.cf_spec_context import ContextError, load_context
381
- from codeflow.spec.cf_stage_gate import evidence_deferral_kind
382
- if not context.is_file():
383
- return 0
384
- try:
385
- data = load_context(str(context))
386
- except (OSError, ValueError, ContextError) as exc:
387
- sys.stderr.write(f"cf_stats: cannot read {context}: {exc}\n")
388
- return 0
389
- return sum(any(
390
- status.status == "unverified" and bool(status.evidence)
391
- and evidence_deferral_kind(status.evidence[-1]) in ("review_stage", "heavy")
392
- for status in rule.stage_status.values()
393
- ) for binding in data.bindings for rule in binding.rules)
394
-
395
-
396
- def _runtime_usage(root: str) -> dict[str, object]:
397
- """Counts describe unique checks, live risk acceptances and deferred rules."""
398
- from codeflow.evidence.cf_evidence_store import EvidenceStore, requirement_key
399
- marker = Path(root) / ".code-flow/.active-task.json"
400
- task_dir = ""
401
- if marker.is_file():
402
- try:
403
- data = json.loads(marker.read_text(encoding="utf-8"))
404
- task_dir = str(data.get("task_dir") or "") if isinstance(data, dict) else ""
405
- except (OSError, ValueError) as exc:
406
- sys.stderr.write(f"cf_stats: cannot read {marker}: {exc}\n")
407
- runtime: dict[str, object] = {
408
- "task_dir": task_dir, "context_bytes": 0, "evidence_runs": 0,
409
- "deferred_rules": 0, "stale_checks": 0, "accepted_risks": 0,
410
- "recorded_risks": 0, "inactive_risks": 0,
411
- }
412
- if not task_dir:
413
- return runtime
414
- directory = (Path(root) / task_dir).resolve()
415
- context = directory / "spec-context.yml"
416
- if context.is_file():
417
- runtime["context_bytes"] = context.stat().st_size
418
- try:
419
- store = EvidenceStore(root, requirement=requirement_key(directory))
420
- runtime["evidence_runs"] = sum(
421
- 1 for path in store.directory.rglob("*.json")
422
- if path.name != "latest.json" and "_context" not in path.parts
423
- )
424
- except (OSError, ValueError) as exc:
425
- sys.stderr.write(f"cf_stats: cannot count evidence: {exc}\n")
426
- runtime.update(_runtime_manifest_counts(directory / ".acceptance-manifest.json"))
427
- runtime["deferred_rules"] = _deferred_rule_count(context)
428
- return runtime
429
-
430
-
431
- def _stats_report(root: str, args: argparse.Namespace) -> dict[str, object]:
432
- config = load_config(root)
433
- budget = config.get("budget") or {}
434
- l0_budget = _budget_number(budget, "l0_max", 800)
435
- l1_budget = _budget_number(budget, "l1_max", 1700)
436
- total_budget = _budget_number(budget, "total", l0_budget + l1_budget)
437
- instruction = project_instruction_file(root, args.platform)
438
- instruction_path = os.path.join(root, instruction)
439
- l0_tokens = estimate_tokens(read_text(instruction_path)) if os.path.exists(instruction_path) else 0
440
- usage = _domain_usage(root, config, args.domain)
441
- total = l0_tokens + usage["tokens"]
442
- report = {
443
- "l0": {"file": instruction, "tokens": l0_tokens, "budget": l0_budget},
444
- "l1": usage["l1"], "total_tokens": total, "total_budget": total_budget,
445
- "budget_summary": {"aggregate_total": usage["tokens"], "single_edit_limit": min(total_budget, l1_budget)},
446
- "utilization": f"{round(total * 100 / total_budget)}%" if total_budget else "0%",
447
- "warnings": _usage_warnings(l0_tokens, l0_budget, l1_budget, total, total_budget, usage),
448
- "spec_domain_map": usage["spec_domain_map"], "missing_specs": usage["missing_specs"],
449
- "compression_summary": _compression_usage(usage["l1"]), "catalog": _catalog_usage(root, config, budget),
450
- "quality_loop": quality_loop_summary(root, config), "spec_workflow": spec_workflow_summary(root),
451
- "templates": {"tokens": usage["template_tokens"], "files": usage["excluded"],
452
- "note": "tags:[] 命令专用模板,永不自动注入,不计预算"},
453
- "runtime": _runtime_usage(root),
454
- }
455
- if args.audit:
456
- from codeflow.spec.cf_scan import build_report
457
- scan = build_report(root, args.platform)
458
- report["audit"] = {"files": [entry for entry in scan["files"] if entry.get("issues")],
459
- "review": scan["review"], "warnings": scan["audit_warnings"],
460
- "path_resolutions": [{"path": entry["path"], "resolved_paths": entry["resolved_paths"]}
461
- for entry in scan["files"] if entry.get("resolved_paths")]}
462
- return report
463
-
464
-
465
- def _print_domains(report: dict[str, object]) -> None:
466
- for domain, items in report["l1"].items():
467
- print(f"L1 {domain}:", sum(item["tokens"] for item in items if item.get("injectable", True)))
468
- for item in items:
469
- raw, compressed = item.get("tokens_raw", item["tokens"]), item.get("tokens_compressed", item["tokens"])
470
- suffix = "" if item.get("injectable", True) else "(模板,不计预算)"
471
- print(" -", item["path"], item["tokens"], f"(raw={raw}→compressed={compressed}, -{item.get('saved_pct', 0.0)}%)" + suffix)
472
- if report["templates"]["tokens"]:
473
- print("TEMPLATES (非注入):", f"{report['templates']['tokens']} tokens,不计预算")
474
- if report["missing_specs"]:
475
- print("MISSING SPECS:")
476
- for item in report["missing_specs"]:
477
- print(" -", item["domain"], item["path"])
478
-
479
-
480
- def _print_quality(summary: dict[str, object]) -> None:
481
- print("QUALITY-LOOP:", "enabled" if summary["switches"]["enabled"] else "disabled")
482
- if summary.get("note"):
483
- print(" -", summary["note"])
484
- return
485
- print(" - violations:", summary.get("violation_total", 0), "| fix_rate:", summary.get("fix_rate", "n/a"))
486
- for item in summary.get("top_violations", [])[:5]:
487
- print(" ·", item["rule"], item["count"])
488
- for check_id, info in summary.get("checks", {}).items():
489
- if info["disabled"] or info["fp_count"]:
490
- suffix = f" DISABLED({info['disabled_reason']})" if info["disabled"] else ""
491
- print(f" · {check_id}: hits={info['hit_count']} fp={info['fp_count']}" + suffix)
492
- for component, info in summary.get("degraded", {}).items():
493
- print(f" · degraded {component}: {info['count']} ({info['last_error']})")
494
-
495
-
496
- def _print_audit(audit: Optional[dict[str, object]]) -> None:
497
- if audit is None:
498
- print("AUDIT: 运行 code-flow stats --audit 查看规范质量问题与待复审清单")
499
- return
500
- print("AUDIT (规范质量):")
501
- if not audit["files"] and not audit["review"] and not audit.get("warnings"):
502
- print(" - 无问题")
503
- for warning in audit.get("warnings", []):
504
- print(f" - 无效审计路径根: {warning['path']}")
505
- for entry in audit["files"]:
506
- print(" -", entry["path"], "|", " / ".join(entry["issues"]))
507
- if audit["review"]:
508
- print(" REVIEW (待复审):")
509
- for item in audit["review"]:
510
- print(f" · {item['item']}: {item['reason']}")
511
-
512
-
513
- def _print_stats(report: dict[str, object]) -> None:
514
- l0, compression, catalog = report["l0"], report["compression_summary"], report["catalog"]
515
- print(f"L0 ({l0['file']}):", f"{l0['tokens']} / {l0['budget']}")
516
- _print_domains(report)
517
- print("TOTAL:", f"{report['total_tokens']} / {report['total_budget']}")
518
- print("UTILIZATION:", report["utilization"])
519
- print("COMPRESSION:", f"{compression['total_raw']} → {compression['total_compressed']} (-{compression['total_saved_pct']}%)")
520
- print("CATALOG:", "mode=context_first,", f"{catalog['tokens']} / {catalog['budget']} tokens,", f"{catalog['entries']} entries")
521
- _print_quality(report["quality_loop"])
522
- runtime = report.get("runtime") or {}
523
- if runtime.get("task_dir"):
524
- print("RUNTIME:", f"context={runtime.get('context_bytes', 0)}B,",
525
- f"evidence_runs={runtime.get('evidence_runs', 0)},",
526
- f"deferred_rules={runtime.get('deferred_rules', 0)},",
527
- f"stale_checks={runtime.get('stale_checks', 0)},",
528
- f"accepted_risks={runtime.get('accepted_risks', 0)}")
529
- if report["warnings"]:
530
- print("WARNINGS:", "; ".join(report["warnings"]))
531
- _print_audit(report.get("audit"))
532
-
533
-
534
- def main() -> None:
535
- args = _arguments()
536
- report = _stats_report(os.getcwd(), args)
537
- if args.human:
538
- _print_stats(report)
539
- else:
540
- sys.stdout.write(json.dumps(report, ensure_ascii=False) + "\n")