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.
- {blq_cli-0.10.2 → blq_cli-0.11.0}/CLAUDE.md +13 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/PKG-INFO +1 -1
- blq_cli-0.11.0/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +126 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/pyproject.toml +1 -1
- blq_cli-0.11.0/src/blq/services/__init__.py +32 -0
- blq_cli-0.11.0/src/blq/services/execution.py +74 -0
- blq_cli-0.11.0/src/blq/services/inspect.py +224 -0
- blq_cli-0.11.0/src/blq/services/query.py +395 -0
- blq_cli-0.11.0/src/blq/services/refs.py +240 -0
- blq_cli-0.11.0/tests/test_services_execution.py +115 -0
- blq_cli-0.11.0/tests/test_services_inspect.py +64 -0
- blq_cli-0.11.0/tests/test_services_query.py +252 -0
- blq_cli-0.11.0/tests/test_services_refs.py +130 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/.github/workflows/ci.yml +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/.github/workflows/docs.yml +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/.github/workflows/publish.yml +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/.gitignore +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/.mcp.json +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/.readthedocs.yml +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/AGENT.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/AGENTS.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/CONTRIBUTING.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/README.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/SKILL.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/capture.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/ci.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/completions.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/errors.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/exec.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/filter.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/index.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/init.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/maintenance.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/query.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/registry.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/report.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/run.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/sql.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/status.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/sync.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/commands/watch.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-commands.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-config-command.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-extensions.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-git-integration.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-run-args.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-sync.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/design-track-save.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/duck-hunt-v3-migration.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/getting-started.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/index.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/integration.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/mcp.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/python-api.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/query-guide.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/requirements.txt +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/sandbox.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/schema.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/experiments/agent-build-test.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/mkdocs.yml +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/__init__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/__main__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/bird.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/bird_schema.sql +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/cli.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/README.md +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/__init__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/core.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/events.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/execution.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/init_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/management.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/mcp_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/migrate.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/registry.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/sandbox_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/sync_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/commands/watch_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/config_format.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/__init__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/annotator.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/discovery.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/local_executor.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/ext/pipeline.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/git.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/github.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/locks.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/output.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/query.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/schema.sql +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/serve.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/storage.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq/user_config.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/__init__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/engines.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/profile.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/source_annotator.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/spec.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/strace_parser.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/tighten.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox/violations.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/src/blq_sandbox_systemd/__init__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/__init__.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/conftest.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_annotator.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_auto_init.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_autoprune.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_bird.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_bwrap_args.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_bwrap_engine.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ci.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ci_generators.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_command_args.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_command_lock_field.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_core.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_execution_locks.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_discovery.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_integration.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_local_executor.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_pipeline.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_ext_types.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_git.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_hooks.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_hooks_gen.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_inspect.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_json_null_filter.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_locks.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_mcp_ci_tools.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_mcp_merge.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_mcp_server.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_query_api.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_query_filter.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_record_invocation.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_report.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_cmd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_events.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_ext.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_profile.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_register.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_systemd.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_tighten.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_sandbox_violations.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_source_annotator.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_storage.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_storage_prune.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_strace_parser.py +0 -0
- {blq_cli-0.10.2 → blq_cli-0.11.0}/tests/test_user_config.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|