blq-cli 0.10.2__tar.gz → 0.11.0__tar.gz

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 (190) hide show
  1. {blq_cli-0.10.2 → blq_cli-0.11.0}/CLAUDE.md +13 -0
  2. {blq_cli-0.10.2 → blq_cli-0.11.0}/PKG-INFO +1 -1
  3. blq_cli-0.11.0/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +126 -0
  4. {blq_cli-0.10.2 → blq_cli-0.11.0}/pyproject.toml +1 -1
  5. blq_cli-0.11.0/src/blq/services/__init__.py +32 -0
  6. blq_cli-0.11.0/src/blq/services/execution.py +74 -0
  7. blq_cli-0.11.0/src/blq/services/inspect.py +224 -0
  8. blq_cli-0.11.0/src/blq/services/query.py +395 -0
  9. blq_cli-0.11.0/src/blq/services/refs.py +240 -0
  10. blq_cli-0.11.0/tests/test_services_execution.py +115 -0
  11. blq_cli-0.11.0/tests/test_services_inspect.py +64 -0
  12. blq_cli-0.11.0/tests/test_services_query.py +252 -0
  13. blq_cli-0.11.0/tests/test_services_refs.py +130 -0
  14. {blq_cli-0.10.2 → blq_cli-0.11.0}/.claude/hooks/blq-suggest.sh +0 -0
  15. {blq_cli-0.10.2 → blq_cli-0.11.0}/.github/workflows/ci.yml +0 -0
  16. {blq_cli-0.10.2 → blq_cli-0.11.0}/.github/workflows/docs.yml +0 -0
  17. {blq_cli-0.10.2 → blq_cli-0.11.0}/.github/workflows/publish.yml +0 -0
  18. {blq_cli-0.10.2 → blq_cli-0.11.0}/.gitignore +0 -0
  19. {blq_cli-0.10.2 → blq_cli-0.11.0}/.mcp.json +0 -0
  20. {blq_cli-0.10.2 → blq_cli-0.11.0}/.readthedocs.yml +0 -0
  21. {blq_cli-0.10.2 → blq_cli-0.11.0}/AGENT.md +0 -0
  22. {blq_cli-0.10.2 → blq_cli-0.11.0}/AGENTS.md +0 -0
  23. {blq_cli-0.10.2 → blq_cli-0.11.0}/CONTRIBUTING.md +0 -0
  24. {blq_cli-0.10.2 → blq_cli-0.11.0}/README.md +0 -0
  25. {blq_cli-0.10.2 → blq_cli-0.11.0}/SKILL.md +0 -0
  26. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/capture.md +0 -0
  27. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/ci.md +0 -0
  28. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/completions.md +0 -0
  29. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/errors.md +0 -0
  30. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/exec.md +0 -0
  31. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/filter.md +0 -0
  32. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/index.md +0 -0
  33. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/init.md +0 -0
  34. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/maintenance.md +0 -0
  35. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/query.md +0 -0
  36. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/registry.md +0 -0
  37. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/report.md +0 -0
  38. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/run.md +0 -0
  39. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/sql.md +0 -0
  40. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/status.md +0 -0
  41. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/sync.md +0 -0
  42. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/watch.md +0 -0
  43. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-bird-migration.md +0 -0
  44. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-commands.md +0 -0
  45. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-config-command.md +0 -0
  46. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-extensions.md +0 -0
  47. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-git-integration.md +0 -0
  48. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-hooks-v2.md +0 -0
  49. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-live-inspection.md +0 -0
  50. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-parameterized-commands.md +0 -0
  51. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-run-args.md +0 -0
  52. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-sandbox-specs.md +0 -0
  53. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-sync.md +0 -0
  54. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-track-save.md +0 -0
  55. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/duck-hunt-v2-migration.md +0 -0
  56. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/duck-hunt-v3-migration.md +0 -0
  57. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/proposal-bird-v5.md +0 -0
  58. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/skill-inspect-enrichment.md +0 -0
  59. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/getting-started.md +0 -0
  60. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/index.md +0 -0
  61. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/integration.md +0 -0
  62. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/mcp.md +0 -0
  63. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
  64. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
  65. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/patterns-integration-prompt.md +0 -0
  66. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/roadmap-to-1.0.md +0 -0
  67. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/python-api.md +0 -0
  68. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/query-guide.md +0 -0
  69. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/requirements.txt +0 -0
  70. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/sandbox.md +0 -0
  71. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/schema.md +0 -0
  72. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
  73. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
  74. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
  75. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
  76. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
  77. {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
  78. {blq_cli-0.10.2 → blq_cli-0.11.0}/experiments/agent-build-test.md +0 -0
  79. {blq_cli-0.10.2 → blq_cli-0.11.0}/mkdocs.yml +0 -0
  80. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/__init__.py +0 -0
  81. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/__main__.py +0 -0
  82. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/bird.py +0 -0
  83. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/bird_schema.sql +0 -0
  84. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/cli.py +0 -0
  85. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/README.md +0 -0
  86. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/__init__.py +0 -0
  87. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/ci_cmd.py +0 -0
  88. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/clean_cmd.py +0 -0
  89. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/config_cmd.py +0 -0
  90. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/core.py +0 -0
  91. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/events.py +0 -0
  92. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/execution.py +0 -0
  93. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/hooks_cmd.py +0 -0
  94. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/hooks_gen.py +0 -0
  95. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/init_cmd.py +0 -0
  96. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/management.py +0 -0
  97. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/management_cmd.py +0 -0
  98. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/mcp_cmd.py +0 -0
  99. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/migrate.py +0 -0
  100. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/query_cmd.py +0 -0
  101. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/record_cmd.py +0 -0
  102. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/registry.py +0 -0
  103. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/report_cmd.py +0 -0
  104. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/sandbox_cmd.py +0 -0
  105. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/serve_cmd.py +0 -0
  106. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/sync_cmd.py +0 -0
  107. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/watch_cmd.py +0 -0
  108. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/config_format.py +0 -0
  109. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/__init__.py +0 -0
  110. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/annotator.py +0 -0
  111. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/discovery.py +0 -0
  112. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/local_executor.py +0 -0
  113. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/pipeline.py +0 -0
  114. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/git.py +0 -0
  115. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/github.py +0 -0
  116. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/locks.py +0 -0
  117. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/output.py +0 -0
  118. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/query.py +0 -0
  119. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/schema.sql +0 -0
  120. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/serve.py +0 -0
  121. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/storage.py +0 -0
  122. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/drone.yml.j2 +0 -0
  123. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/git_hook.sh.j2 +0 -0
  124. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
  125. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
  126. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/hook_script.sh.j2 +0 -0
  127. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/user_config.py +0 -0
  128. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/__init__.py +0 -0
  129. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/engines.py +0 -0
  130. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/profile.py +0 -0
  131. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/source_annotator.py +0 -0
  132. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/spec.py +0 -0
  133. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/strace_parser.py +0 -0
  134. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/tighten.py +0 -0
  135. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/violations.py +0 -0
  136. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
  137. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox_bwrap/args.py +0 -0
  138. {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox_systemd/__init__.py +0 -0
  139. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/__init__.py +0 -0
  140. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/conftest.py +0 -0
  141. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
  142. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_annotator.py +0 -0
  143. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_attempts_outcomes.py +0 -0
  144. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_auto_init.py +0 -0
  145. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_autoprune.py +0 -0
  146. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_bird.py +0 -0
  147. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_bwrap_args.py +0 -0
  148. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_bwrap_engine.py +0 -0
  149. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ci.py +0 -0
  150. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ci_generators.py +0 -0
  151. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_command_args.py +0 -0
  152. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_command_lock_field.py +0 -0
  153. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_core.py +0 -0
  154. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_execution_locks.py +0 -0
  155. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_exit_code_reason.py +0 -0
  156. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_discovery.py +0 -0
  157. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_integration.py +0 -0
  158. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_local_executor.py +0 -0
  159. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_pipeline.py +0 -0
  160. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_types.py +0 -0
  161. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_git.py +0 -0
  162. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_hooks.py +0 -0
  163. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_hooks_gen.py +0 -0
  164. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_inspect.py +0 -0
  165. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_json_null_filter.py +0 -0
  166. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_locks.py +0 -0
  167. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_mcp_ci_tools.py +0 -0
  168. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_mcp_merge.py +0 -0
  169. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_mcp_server.py +0 -0
  170. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_phase1_structured_output.py +0 -0
  171. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_phase2_command_registry.py +0 -0
  172. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_query_api.py +0 -0
  173. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_query_filter.py +0 -0
  174. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_record_invocation.py +0 -0
  175. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_report.py +0 -0
  176. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox.py +0 -0
  177. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_cmd.py +0 -0
  178. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_events.py +0 -0
  179. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_ext.py +0 -0
  180. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_profile.py +0 -0
  181. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_register.py +0 -0
  182. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_systemd.py +0 -0
  183. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_tighten.py +0 -0
  184. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_violations.py +0 -0
  185. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_source_annotator.py +0 -0
  186. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_storage.py +0 -0
  187. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_storage_prune.py +0 -0
  188. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_strace_parser.py +0 -0
  189. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_user_config.py +0 -0
  190. {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_watch.py +0 -0
@@ -125,6 +125,19 @@ blq (Python CLI)
125
125
  └── Optionally uses duck_hunt extension for 60+ format parsing
126
126
  ```
127
127
 
128
+ ### Service Layer
129
+
130
+ Shared business logic in `src/blq/services/`, called by both CLI and MCP:
131
+
132
+ | Module | Functions | Purpose |
133
+ |--------|-----------|---------|
134
+ | `refs.py` | `parse_ref()`, `resolve_run_ref()` | Canonical ref parsing and resolution |
135
+ | `query.py` | `query_status()`, `query_history()`, `query_events()`, `query_diff()` | Database queries |
136
+ | `inspect.py` | `get_source_context()`, `get_log_context()`, `get_git_context()`, `get_fingerprint_history()` | Event context enrichment |
137
+ | `execution.py` | `run_result_to_concise()` | RunResult → concise dict conversion |
138
+
139
+ Services take `BlqStorage` and return structured data. No argparse or MCP coupling.
140
+
128
141
  ### Storage Modes
129
142
 
130
143
  BIRD is the default storage mode. Legacy parquet mode is still supported:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blq-cli
3
- Version: 0.10.2
3
+ Version: 0.11.0
4
4
  Summary: Buidl Log Query - capture and query build/test logs with DuckDB
5
5
  Project-URL: Homepage, https://blq-cli.readthedocs.com/
6
6
  Project-URL: Repository, https://github.com/teaguesterling/blq-cli
@@ -0,0 +1,126 @@
1
+ # Unified Service Layer Design
2
+
3
+ ## Problem
4
+
5
+ CLI and MCP implement the same operations independently, causing:
6
+ - Query results that can diverge silently between CLI and MCP
7
+ - ~500 lines of duplicated logic across serve.py and commands/
8
+ - Every new feature requires parallel implementation in both layers
9
+ - Two parallel ref parsers for the same syntax
10
+ - MCP shells out to `blq run` as a subprocess instead of sharing the execution path
11
+
12
+ ## Architecture
13
+
14
+ A new `src/blq/services/` package containing pure business logic functions. Each function takes a `BlqStorage` instance and query parameters, returns structured data (dicts/lists). No argparse, no MCP, no output formatting.
15
+
16
+ ```
17
+ CLI (argparse) → services/ ← MCP (fastmcp)
18
+
19
+ BlqStorage
20
+
21
+ DuckDB
22
+ ```
23
+
24
+ **The rule:** Services return data. CLI formats it for the terminal. MCP serializes it as JSON. Neither the CLI nor MCP contains query logic.
25
+
26
+ ## Approach
27
+
28
+ **Bottom-up extraction.** Start with shared query logic (inspect helpers, history, ref resolution), leave execution as subprocess for now. The subprocess gives crash isolation; eliminating it is a follow-up.
29
+
30
+ **serve.py stays as one file** but gets thinner. After extraction, `_*_impl()` functions become thin adapters calling service functions. Splitting serve.py into `mcp/` modules is a follow-up once the service layer is stable.
31
+
32
+ ## Module Layout
33
+
34
+ ```
35
+ src/blq/services/
36
+ ├── __init__.py # Re-exports for convenience
37
+ ├── refs.py # Canonical ref resolver
38
+ ├── query.py # status, history, events, diff
39
+ ├── inspect.py # Event inspection with context layers
40
+ ├── execution.py # RunResult → concise dict conversion
41
+ └── commands.py # list/register/unregister operations
42
+ ```
43
+
44
+ ## Service Function Signatures
45
+
46
+ All services take `BlqStorage` as first argument (caller opens and passes it):
47
+
48
+ ```python
49
+ # refs.py
50
+ def parse_ref(ref: str) -> ParsedRef
51
+ def resolve_run(storage: BlqStorage, ref: str) -> dict | None
52
+
53
+ # query.py
54
+ def query_status(storage: BlqStorage) -> list[dict]
55
+ def query_history(storage: BlqStorage, limit: int, source: str | None, status: str | None) -> list[dict]
56
+ def query_events(storage: BlqStorage, severity: str | None, run_id: int | None,
57
+ source: str | None, file_pattern: str | None, limit: int) -> dict
58
+ def query_diff(storage: BlqStorage, run1: int, run2: int) -> dict
59
+
60
+ # inspect.py
61
+ def inspect_event(storage: BlqStorage, ref: str, source_root: Path,
62
+ include_source: bool, include_git: bool, include_fingerprint: bool) -> dict
63
+
64
+ # execution.py
65
+ def run_result_to_concise(result: dict, source_name: str) -> dict
66
+
67
+ # commands.py
68
+ def list_commands(config: BlqConfig) -> list[dict]
69
+ def register_command(config: BlqConfig, name: str, cmd: str, **kwargs) -> dict
70
+ def unregister_command(config: BlqConfig, name: str) -> dict
71
+ ```
72
+
73
+ ## What Gets Extracted
74
+
75
+ ### refs.py — Canonical ref resolver
76
+ Merges `resolve_ref()` from `management.py:35` and `_parse_ref()` + `_parse_run_ref()` from `serve.py:227/262`. Two parallel parsers for the same `tag:serial:event` syntax become one.
77
+
78
+ ### query.py — Status, history, events, diff
79
+ - **status**: Merges `cmd_status()` (management.py) and `_status_impl()` (serve.py). Shared: `status_str` computation, `run_ref` construction.
80
+ - **history**: Merges `cmd_history()` and `_history_impl()`. SQL query and status mapping are near-identical.
81
+ - **events**: Merges `cmd_events()` and `_errors_impl()`/`_warnings_impl()`/`_events_impl()`. WHERE building, suppression logic, event-dict shaping.
82
+ - **diff**: Currently MCP-only. Service function makes it available to CLI too.
83
+
84
+ ### inspect.py — Event inspection with context
85
+ Moves four private helpers from `events.py` into shared functions: `_get_log_context()`, `_get_source_context()`, `_get_git_context()`, `_get_fingerprint_history()`. Eliminates ~180 lines of duplication in serve.py.
86
+
87
+ ### execution.py — Result conversion
88
+ Extracts the `RunResult → concise dict` shaping from `_run_impl()` and `_exec_impl()` in serve.py (~100 lines duplicated between the two). Subprocess bridge stays; only the response shaping is shared.
89
+
90
+ ### commands.py — Command registry
91
+ Extracts command list/register/unregister logic that's currently in both `registry.py` and serve.py's `_register_command_impl()`.
92
+
93
+ ## Migration Strategy
94
+
95
+ **Phase 1: Extract services (no callers changed)**
96
+ - Create `services/` modules with shared logic
97
+ - Write tests for each service function independently
98
+ - Existing CLI and MCP untouched
99
+
100
+ **Phase 2: Wire CLI to services**
101
+ - CLI handlers call service functions, keep only argparse + formatting
102
+ - Run full test suite after each command migration
103
+
104
+ **Phase 3: Wire MCP to services**
105
+ - MCP `_*_impl()` functions call service functions
106
+ - Subprocess bridge for run/exec stays
107
+ - serve.py shrinks from ~4,400 to ~2,000 lines
108
+
109
+ **Phase 4: Cleanup**
110
+ - Remove dead code
111
+ - Verify CLI and MCP produce identical results
112
+
113
+ ## What This Does NOT Do
114
+
115
+ - **No direct execution from MCP** — subprocess bridge stays (crash isolation)
116
+ - **No serve.py split** — stays as one file, just thinner
117
+ - **No new features** — pure refactor, behavior unchanged
118
+ - **No storage layer changes** — BlqStorage stays as-is
119
+
120
+ ## Success Criteria
121
+
122
+ - All existing tests pass with no changes
123
+ - CLI and MCP produce identical results for: status, history, events, info, inspect, diff
124
+ - serve.py shrinks by ~1,500 lines
125
+ - No query logic remains in CLI command handlers or MCP `_*_impl()` functions
126
+ - One canonical ref resolver used everywhere
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "blq-cli"
7
- version = "0.10.2"
7
+ version = "0.11.0"
8
8
  description = "Buidl Log Query - capture and query build/test logs with DuckDB"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,32 @@
1
+ """Shared service layer for blq.
2
+
3
+ Services contain pure business logic called by both CLI and MCP.
4
+ Each function takes a BlqStorage instance and returns structured data.
5
+ No argparse, no MCP, no output formatting.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from blq.services.execution import run_result_to_concise
10
+ from blq.services.inspect import (
11
+ get_fingerprint_history,
12
+ get_git_context,
13
+ get_log_context,
14
+ get_source_context,
15
+ )
16
+ from blq.services.query import query_diff, query_events, query_history, query_status
17
+ from blq.services.refs import ParsedRef, parse_ref, resolve_run_ref
18
+
19
+ __all__ = [
20
+ "ParsedRef",
21
+ "parse_ref",
22
+ "resolve_run_ref",
23
+ "query_status",
24
+ "query_history",
25
+ "query_events",
26
+ "query_diff",
27
+ "get_source_context",
28
+ "get_log_context",
29
+ "get_git_context",
30
+ "get_fingerprint_history",
31
+ "run_result_to_concise",
32
+ ]
@@ -0,0 +1,74 @@
1
+ """Execution services for blq.
2
+
3
+ Provides shared business logic for shaping RunResult JSON into the concise
4
+ format returned to MCP callers and CLI consumers.
5
+ No argparse, no MCP, no output formatting.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ def run_result_to_concise(full_result: dict[str, Any], source_name: str) -> dict[str, Any]:
14
+ """Convert a full RunResult JSON dict into the concise MCP response format.
15
+
16
+ The ``full_result`` is the dict produced by ``RunResult.to_json()`` (already
17
+ parsed from JSON). ``source_name`` is used to build ``run_ref`` when the
18
+ result does not contain one (e.g. the caller knows the registered command
19
+ name but the result only has a numeric run_id).
20
+
21
+ Always-present keys in the returned dict:
22
+ run_ref – "<source_name>:<run_id>" or None when run_id is absent
23
+ cmd – the command string from full_result["command"]
24
+ status – from full_result or derived from exit_code
25
+ exit_code – int
26
+ duration_sec – float rounded to 1 decimal place
27
+ summary – dict from full_result
28
+ output_stats – {"lines": int, "bytes": int}
29
+
30
+ Conditionally-present keys (only when the value is non-empty/non-None):
31
+ status_reason – str
32
+ errors – first 10 items
33
+ warnings – first 5 items
34
+ infos – first 5 items
35
+ """
36
+ run_id = full_result.get("run_id")
37
+ exit_code = full_result.get("exit_code", 0)
38
+ status = full_result.get("status", "FAIL" if exit_code != 0 else "OK")
39
+ output_stats_raw = full_result.get("output_stats", {})
40
+
41
+ concise: dict[str, Any] = {
42
+ "run_ref": f"{source_name}:{run_id}" if run_id is not None else None,
43
+ "cmd": full_result.get("command"),
44
+ "status": status,
45
+ "exit_code": exit_code,
46
+ "duration_sec": round(full_result.get("duration_sec", 0), 1),
47
+ "summary": full_result.get("summary", {}),
48
+ "output_stats": {
49
+ "lines": output_stats_raw.get("lines", 0),
50
+ "bytes": output_stats_raw.get("bytes", 0),
51
+ },
52
+ }
53
+
54
+ # Include status_reason when present and non-None
55
+ status_reason = full_result.get("status_reason")
56
+ if status_reason:
57
+ concise["status_reason"] = status_reason
58
+
59
+ # Include errors (capped at 10, summary has total count)
60
+ errors = full_result.get("errors", [])
61
+ if errors:
62
+ concise["errors"] = errors[:10]
63
+
64
+ # Include warnings (top 5)
65
+ warnings = full_result.get("warnings", [])
66
+ if warnings:
67
+ concise["warnings"] = warnings[:5]
68
+
69
+ # Include info/summary events (top 5)
70
+ infos = full_result.get("infos", [])
71
+ if infos:
72
+ concise["infos"] = infos[:5]
73
+
74
+ return concise
@@ -0,0 +1,224 @@
1
+ """Inspect service: context-fetching functions for event enrichment.
2
+
3
+ Provides source, log, git, and fingerprint context for events.
4
+ Used by both CLI (commands/events.py) and MCP (serve.py).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from blq.storage import BlqStorage
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def get_source_context(
20
+ ref_file: str | None,
21
+ ref_line: int | None,
22
+ source_root: Path,
23
+ context_lines: int = 3,
24
+ ) -> str | None:
25
+ """Get source file context around a specific line.
26
+
27
+ Args:
28
+ ref_file: Path to the source file (relative to source_root).
29
+ ref_line: 1-indexed line number of the event.
30
+ source_root: Root path for resolving relative file paths.
31
+ context_lines: Number of lines before/after to include.
32
+
33
+ Returns:
34
+ Formatted context string with line numbers and markers, or None.
35
+ """
36
+ if ref_file is None or ref_line is None:
37
+ return None
38
+
39
+ try:
40
+ import blq.output as output_mod
41
+
42
+ return output_mod.read_source_context(
43
+ ref_file,
44
+ ref_line,
45
+ ref_root=str(source_root),
46
+ context=context_lines,
47
+ )
48
+ except Exception:
49
+ logger.debug("Failed to get source context for %s:%s", ref_file, ref_line, exc_info=True)
50
+ return None
51
+
52
+
53
+ def get_log_context(
54
+ storage: BlqStorage | None,
55
+ run_id: int,
56
+ log_line_start: int | None,
57
+ log_line_end: int | None,
58
+ context_lines: int = 3,
59
+ ) -> str | None:
60
+ """Get log output context around the event's line range.
61
+
62
+ Args:
63
+ storage: BlqStorage instance with get_output().
64
+ run_id: The run ID to fetch output for.
65
+ log_line_start: 1-indexed start line of the event in the log.
66
+ log_line_end: 1-indexed end line of the event in the log.
67
+ context_lines: Number of lines before/after to include.
68
+
69
+ Returns:
70
+ Formatted context string, or None.
71
+ """
72
+ if log_line_start is None or log_line_end is None:
73
+ return None
74
+
75
+ if storage is None:
76
+ return None
77
+
78
+ try:
79
+ import blq.output as output_mod
80
+
81
+ output_bytes = storage.get_output(run_id)
82
+ if output_bytes is None:
83
+ return None
84
+
85
+ content = output_bytes.decode("utf-8", errors="replace")
86
+ lines = content.splitlines()
87
+ return output_mod.format_context(
88
+ lines,
89
+ log_line_start,
90
+ log_line_end,
91
+ context=context_lines,
92
+ )
93
+ except Exception:
94
+ logger.debug("Failed to get log context for run %s", run_id, exc_info=True)
95
+ return None
96
+
97
+
98
+ def get_git_context(
99
+ ref_file: str | None,
100
+ ref_line: int | None,
101
+ source_root: Path,
102
+ history_limit: int = 2,
103
+ ) -> dict[str, Any] | None:
104
+ """Get git blame and history context for a source file location.
105
+
106
+ Args:
107
+ ref_file: Path to the source file (relative to source_root).
108
+ ref_line: Line number for blame info (optional).
109
+ source_root: Root path for resolving relative file paths.
110
+ history_limit: Maximum number of recent commits to include.
111
+
112
+ Returns:
113
+ Dict with file, line, blame, and recent_commits, or None.
114
+ """
115
+ if ref_file is None:
116
+ return None
117
+
118
+ try:
119
+ import blq.git as git_mod
120
+
121
+ file_path = source_root / ref_file
122
+ if not file_path.exists():
123
+ return None
124
+
125
+ ctx = git_mod.get_file_context(str(file_path), line=ref_line, history_limit=history_limit)
126
+
127
+ result: dict[str, Any] = {
128
+ "file": ref_file,
129
+ "line": ref_line,
130
+ }
131
+
132
+ if ctx.last_author:
133
+ result["blame"] = {
134
+ "author": ctx.last_author,
135
+ "commit": ctx.last_commit,
136
+ "modified": ctx.last_modified.isoformat() if ctx.last_modified else None,
137
+ }
138
+
139
+ if ctx.recent_commits:
140
+ result["recent_commits"] = [
141
+ {
142
+ "hash": c.short_hash,
143
+ "author": c.author,
144
+ "time": c.time.isoformat() if c.time else None,
145
+ "message": c.message,
146
+ }
147
+ for c in ctx.recent_commits
148
+ ]
149
+
150
+ return result
151
+ except Exception:
152
+ logger.debug("Failed to get git context for %s", ref_file, exc_info=True)
153
+ return None
154
+
155
+
156
+ def get_fingerprint_history(
157
+ storage: BlqStorage | None,
158
+ fingerprint: str | None,
159
+ ) -> dict[str, Any] | None:
160
+ """Get occurrence history for an event fingerprint.
161
+
162
+ Queries all events with the same fingerprint and returns summary info
163
+ including first/last seen and regression detection.
164
+
165
+ Args:
166
+ storage: BlqStorage instance with sql() method.
167
+ fingerprint: The fingerprint value to look up.
168
+
169
+ Returns:
170
+ Dict with fingerprint, first_seen, last_seen, occurrences,
171
+ and is_regression, or None.
172
+ """
173
+ if fingerprint is None:
174
+ return None
175
+
176
+ if storage is None:
177
+ return None
178
+
179
+ try:
180
+ result = storage.sql(
181
+ """
182
+ SELECT
183
+ run_serial,
184
+ run_ref,
185
+ timestamp,
186
+ tag
187
+ FROM blq_load_events()
188
+ WHERE fingerprint = ?
189
+ ORDER BY started_at ASC
190
+ """,
191
+ [fingerprint],
192
+ ).fetchall()
193
+
194
+ if not result:
195
+ return None
196
+
197
+ first = result[0]
198
+ last = result[-1]
199
+
200
+ # Detect regression: gap > 1 in consecutive run_serial values
201
+ is_regression = False
202
+ if len(result) >= 2:
203
+ run_serials = [r[0] for r in result]
204
+ for i in range(1, len(run_serials)):
205
+ if run_serials[i] - run_serials[i - 1] > 1:
206
+ is_regression = True
207
+ break
208
+
209
+ return {
210
+ "fingerprint": fingerprint[:16] + "..." if len(fingerprint) > 16 else fingerprint,
211
+ "first_seen": {
212
+ "run_ref": first[1],
213
+ "timestamp": first[2].isoformat() if first[2] else None,
214
+ },
215
+ "last_seen": {
216
+ "run_ref": last[1],
217
+ "timestamp": last[2].isoformat() if last[2] else None,
218
+ },
219
+ "occurrences": len(result),
220
+ "is_regression": is_regression,
221
+ }
222
+ except Exception:
223
+ logger.debug("Failed to get fingerprint history for %s", fingerprint, exc_info=True)
224
+ return None