blq-cli 1.0.2__tar.gz → 1.2.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-1.0.2 → blq_cli-1.2.0}/AGENTS.md +1 -1
- blq_cli-1.2.0/CHANGELOG.md +113 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/PKG-INFO +2 -2
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/duck-hunt-v3-migration.md +4 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/schema.md +1 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/pyproject.toml +1 -1
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/bird.py +91 -42
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/bird_schema.sql +4 -1
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/core.py +48 -4
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/execution.py +38 -3
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/migrate.py +1 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/__init__.py +31 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/local_executor.py +4 -2
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/output.py +35 -11
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/serve.py +166 -28
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/inspect.py +5 -2
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/query.py +20 -15
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/profile.py +15 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_core.py +12 -3
- blq_cli-1.2.0/tests/test_import_capture_storage.py +196 -0
- blq_cli-1.2.0/tests/test_log_content_persistence.py +345 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_mcp_ci_tools.py +12 -1
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_mcp_server.py +12 -1
- blq_cli-1.2.0/tests/test_output_schemas.py +90 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_schema_repair.py +8 -4
- blq_cli-1.2.0/tests/test_security_containment.py +239 -0
- blq_cli-1.0.2/CHANGELOG.md +0 -6
- {blq_cli-1.0.2 → blq_cli-1.2.0}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/.github/workflows/ci.yml +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/.github/workflows/docs.yml +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/.github/workflows/publish.yml +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/.gitignore +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/.mcp.json +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/.readthedocs.yml +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/AGENT.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/CLAUDE.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/CONTRIBUTING.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/README.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/SKILL.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/api-reference.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/capture.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/ci.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/completions.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/errors.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/exec.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/filter.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/index.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/init.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/maintenance.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/query.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/registry.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/report.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/run.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/sql.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/status.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/sync.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/watch.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-commands.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-config-command.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-extensions.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-git-integration.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-run-args.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-sync.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-track-save.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/getting-started.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/index.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/integration.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/mcp.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/explore-bird-sqlite-backend.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plugin-guide.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/python-api.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/query-guide.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/requirements.txt +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/sandbox.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/schema-stability.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/experiments/agent-build-test.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/mkdocs.yml +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/__init__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/__main__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/cli.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/README.md +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/__init__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/events.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/init_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/management.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/mcp_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/registry.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/sandbox_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/sync_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/watch_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/config_format.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/annotator.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/discovery.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/pipeline.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/git.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/github.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/locks.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/query.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/runtime.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/schema.sql +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/__init__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/execution.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/refs.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/storage.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/user_config.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/__init__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/engines.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/source_annotator.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/spec.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/strace_parser.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/tighten.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/violations.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox_systemd/__init__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/__init__.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/conftest.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_annotator.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_auto_init.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_autoprune.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_bird.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_bwrap_args.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_bwrap_engine.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ci.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ci_generators.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_command_args.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_command_lock_field.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_config_consumption.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_execution_locks.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_discovery.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_integration.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_local_executor.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_pipeline.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_types.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_git.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_hooks.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_hooks_gen.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_inspect.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_json_null_filter.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_locks.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_mcp_merge.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_query_api.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_query_filter.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_record_invocation.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_report.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_runtime_config.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_cmd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_events.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_ext.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_profile.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_register.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_systemd.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_tighten.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_violations.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_execution.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_inspect.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_query.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_refs.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_source_annotator.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_storage.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_storage_prune.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_strace_parser.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_user_config.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_watch.py +0 -0
- {blq_cli-1.0.2 → blq_cli-1.2.0}/uv.lock +0 -0
|
@@ -560,7 +560,7 @@ Pre-built prompts that guide agents through common workflows:
|
|
|
560
560
|
"file_path": "src/main.c",
|
|
561
561
|
"line_number": 15,
|
|
562
562
|
"message": "undefined variable 'foo'",
|
|
563
|
-
"
|
|
563
|
+
"log_content": "src/main.c:15:5: error: use of undeclared identifier 'foo'",
|
|
564
564
|
"fingerprint": "abc123...",
|
|
565
565
|
"cwd": "/home/user/project",
|
|
566
566
|
"hostname": "dev-machine",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v1.2.0
|
|
4
|
+
|
|
5
|
+
### Added — MCP tools publish the names of the keys they return (#47)
|
|
6
|
+
|
|
7
|
+
fastmcp derives `outputSchema` from a tool's return annotation, so
|
|
8
|
+
`dict[str, Any]` published `{"type": "object", "additionalProperties": true}`
|
|
9
|
+
— an object with no key names. A programmatic caller had to guess them, and a
|
|
10
|
+
wrong guess produces a program that validates, runs, and answers incorrectly.
|
|
11
|
+
Measured across four local models driven through lackpy: **0/24 correct while
|
|
12
|
+
17/24 called the right tool**, with `len()` on `events`' dict returning its key
|
|
13
|
+
count rather than its event count.
|
|
14
|
+
|
|
15
|
+
14 tools now return TypedDicts whose keys come from the implementations' own
|
|
16
|
+
return statements. Declaring the shapes immediately caught three places where
|
|
17
|
+
the code did not match a reasonable reading of it:
|
|
18
|
+
|
|
19
|
+
- `output.streams` is a list of stream names, not a mapping
|
|
20
|
+
- `query.rows` is positional value lists paired with `columns`, not per-row objects
|
|
21
|
+
- `ci_generate.available` is the registered command names, not a boolean
|
|
22
|
+
|
|
23
|
+
No data is dropped — keys a TypedDict does not declare still reach the client
|
|
24
|
+
in `structuredContent`.
|
|
25
|
+
|
|
26
|
+
**One behavioural note for Python callers:** with an `outputSchema` present,
|
|
27
|
+
fastmcp's own client deserializes `.data` into a generated model rather than a
|
|
28
|
+
dict, so `result.data["key"]` no longer works. Read `structured_content`, which
|
|
29
|
+
is the JSON a real MCP client receives over the wire. Consumers speaking
|
|
30
|
+
JSON-RPC (Claude Code, lackpy, any stdio client) are unaffected and strictly
|
|
31
|
+
better off.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## v1.1.0
|
|
35
|
+
|
|
36
|
+
Three fixes with one theme: blq reported success while losing the thing it was
|
|
37
|
+
asked to capture. Schema moves 3.0.0 -> 3.1.0 and migrates automatically on the
|
|
38
|
+
next open; no user action, existing rows are preserved.
|
|
39
|
+
|
|
40
|
+
### Fixed — `import` and `capture` wrote data no query could read (#50)
|
|
41
|
+
|
|
42
|
+
`cmd_import` and `cmd_capture` called `write_run_parquet` unconditionally, while
|
|
43
|
+
`exec`/`run` branch on `config.use_bird`. BIRD is the default storage mode, so on
|
|
44
|
+
a default project an import produced a well-formed parquet under `.bird/logs/`
|
|
45
|
+
that the query layer — a view over the BIRD tables — never reads:
|
|
46
|
+
|
|
47
|
+
$ blq import report.json --format pytest_json
|
|
48
|
+
Imported 2 events (2 errors, 0 warnings)
|
|
49
|
+
Saved to .bird/logs/date=.../source=import/001_report.parquet
|
|
50
|
+
$ blq events
|
|
51
|
+
(no data)
|
|
52
|
+
|
|
53
|
+
Success, a destination path, exit 0, a real file, and no observable effect. The
|
|
54
|
+
storage branch had been added to the exec path and never to these two. Both now
|
|
55
|
+
route through the configured backend; parquet mode is unchanged.
|
|
56
|
+
|
|
57
|
+
This mattered most for the richest source available: a `pytest_json` import
|
|
58
|
+
carries the whole failure block — assertion, source line, and the
|
|
59
|
+
`+ where None = normalize(...)` line naming the function under test — where a
|
|
60
|
+
default text capture of the same run carried seven characters.
|
|
61
|
+
|
|
62
|
+
### Fixed — captured children were told the terminal was 80 columns (#49)
|
|
63
|
+
|
|
64
|
+
Anything that formats for a terminal consults `COLUMNS` and falls back to 80
|
|
65
|
+
without a TTY, which is always true under a capturing runner. `pytest -q`
|
|
66
|
+
abbreviates its short-summary line on that basis, and blq parses that line, so
|
|
67
|
+
the stored message lost everything past roughly 80 columns minus the test name.
|
|
68
|
+
How diagnosable a failure was depended on how long someone had named the test:
|
|
69
|
+
|
|
70
|
+
| test name | message stored |
|
|
71
|
+
|---|---|
|
|
72
|
+
| 73 characters | 7 — `asse...` |
|
|
73
|
+
| 35 characters | 45 — nothing lost |
|
|
74
|
+
|
|
75
|
+
Captured children now receive `COLUMNS=200` unless the caller set one; an
|
|
76
|
+
explicit `COLUMNS=80` still truncates, which is the caller's choice. Applied in
|
|
77
|
+
`blq.ext.capture_env`, covering `LocalExecutor` (the path `exec`/`run` take) and
|
|
78
|
+
the two direct `Popen` sites, plus the sandbox profiler for consistency.
|
|
79
|
+
|
|
80
|
+
### Fixed — duck_hunt's `log_content` was received and discarded (#52)
|
|
81
|
+
|
|
82
|
+
duck_hunt returns the entire failure block in `log_content`. blq queries
|
|
83
|
+
`SELECT *` so it received the column, then dropped it at write time because the
|
|
84
|
+
event schema had no place for it — keeping only `log_line_start`/`log_line_end`,
|
|
85
|
+
pointers into a log not retained unless `--keep-raw` is set. Measured on one
|
|
86
|
+
assertion failure: **391 characters available, 16 persisted.**
|
|
87
|
+
|
|
88
|
+
`events.log_content` now exists in both backends, exposed through
|
|
89
|
+
`blq_load_events()`, capped at 32K with the truncation stated in the value
|
|
90
|
+
rather than applied silently.
|
|
91
|
+
|
|
92
|
+
The vestigial `raw_text` column was renamed rather than duplicated: nothing
|
|
93
|
+
wrote it, its only reader looked up a key duck_hunt does not emit, and
|
|
94
|
+
`docs/design/duck-hunt-v3-migration.md` already recorded `raw_text -> log_content`
|
|
95
|
+
as the intended migration. Old parquet files stay readable via
|
|
96
|
+
`union_by_name = true`. The MCP `event` tool now returns a populated
|
|
97
|
+
`log_content` where it previously returned an always-null `raw_text`.
|
|
98
|
+
|
|
99
|
+
### Migration
|
|
100
|
+
|
|
101
|
+
Schema 3.0.0 -> 3.1.0, handled by the existing self-heal on next open. Two
|
|
102
|
+
layers, following the `extension_data` precedent: a version-keyed migration that
|
|
103
|
+
drops dependent views before the `ALTER`, and a version-independent column
|
|
104
|
+
reconcile so a database whose version advanced without the `ALTER` landing is
|
|
105
|
+
still repaired. Verified against a database built from the shipped 3.0.0 schema
|
|
106
|
+
with prior data: version advances, existing rows survive with NULL, new rows
|
|
107
|
+
carry the block.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## v1.0.2
|
|
111
|
+
|
|
112
|
+
### Bug fixes
|
|
113
|
+
- **Self-heal DBs stuck without `extension_data`.** The 2.3->2.4 migration renamed `sandbox` -> `extension_data` via `RENAME COLUMN`, which DuckDB blocks when non-view dependencies (FKs/constraints) exist on `attempts`/`invocations` — even after dropping views. The rename failed silently but the schema version advanced anyway, leaving DBs permanently stuck without `extension_data` and crashing every `write_attempt` ("Table attempts does not have a column extension_data"). `_ensure_schema` now self-heals version-independently: it gates on a missing column, adds `extension_data` via `ADD COLUMN` (never blocked), copies any existing `sandbox` data across (wrapped as `{"sandbox": ...}`, no data loss), and re-applies the idempotent schema to recreate views. Healthy/fresh DBs stay a fast no-op. Repairs on next connect (the CLI heals on the next `blq run`; a running MCP server heals after restart).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: blq-cli
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Build Log Query - capture and query build/test logs with DuckDB
|
|
5
5
|
Project-URL: Homepage, https://blq-cli.readthedocs.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/teaguesterling/blq-cli
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
This document outlines the changes needed to migrate blq to duck_hunt's Schema V3.
|
|
6
6
|
|
|
7
|
+
> **Status:** the `ref_*` renames and the `raw_text` -> `log_content` rename have
|
|
8
|
+
> landed (the latter in #52, which also began populating the column in both
|
|
9
|
+
> storage backends). The `log_file` field is still not persisted.
|
|
10
|
+
|
|
7
11
|
## Key Changes
|
|
8
12
|
|
|
9
13
|
### 1. Field Renames
|
|
@@ -86,6 +86,7 @@ Parsed diagnostics (errors, warnings, test results).
|
|
|
86
86
|
| `fingerprint` | VARCHAR | Unique identifier for deduplication |
|
|
87
87
|
| `log_line_start` | INTEGER | Start line in raw log |
|
|
88
88
|
| `log_line_end` | INTEGER | End line in raw log |
|
|
89
|
+
| `log_content` | VARCHAR | Raw log block for this event (e.g. the whole pytest FAILURES block); capped at 32768 chars, with a visible marker when truncated |
|
|
89
90
|
| `context` | VARCHAR | Surrounding context |
|
|
90
91
|
| `metadata` | JSON | Format-specific extras |
|
|
91
92
|
| `format_used` | VARCHAR | Parser format (gcc, cargo, pytest) |
|
|
@@ -32,6 +32,34 @@ logger = logging.getLogger("blq-bird")
|
|
|
32
32
|
# Type variable for retry function
|
|
33
33
|
T = TypeVar("T")
|
|
34
34
|
|
|
35
|
+
# Current BIRD schema version. Must match the value seeded by bird_schema.sql;
|
|
36
|
+
# _needs_repair/_apply_migrations compare a database's stored version against it.
|
|
37
|
+
SCHEMA_VERSION = "3.1.0"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _version_tuple(version: str) -> tuple[int, int]:
|
|
41
|
+
"""(major, minor) of a version string, or (0, 0) if unparseable."""
|
|
42
|
+
try:
|
|
43
|
+
parts = [int(p) for p in version.split(".")]
|
|
44
|
+
except (ValueError, AttributeError):
|
|
45
|
+
return (0, 0)
|
|
46
|
+
if not parts:
|
|
47
|
+
return (0, 0)
|
|
48
|
+
return (parts[0], parts[1] if len(parts) > 1 else 0)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
LATEST_SCHEMA = _version_tuple(SCHEMA_VERSION)
|
|
52
|
+
|
|
53
|
+
# Columns that must exist on a table regardless of the recorded schema version.
|
|
54
|
+
# A migration can fail silently (DuckDB blocks ALTER while views depend on the
|
|
55
|
+
# table) and let the version advance past itself, so these are also checked
|
|
56
|
+
# directly and re-added by _reconcile_schema.
|
|
57
|
+
REQUIRED_COLUMNS: dict[str, dict[str, str]] = {
|
|
58
|
+
"attempts": {"extension_data": "JSON"},
|
|
59
|
+
"invocations": {"extension_data": "JSON"},
|
|
60
|
+
"events": {"log_content": "VARCHAR"},
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
# Default retry settings for lock contention
|
|
36
64
|
DEFAULT_MAX_RETRIES = 5
|
|
37
65
|
DEFAULT_INITIAL_DELAY = 0.05 # 50ms
|
|
@@ -99,8 +127,9 @@ def retry_on_lock(
|
|
|
99
127
|
raise last_error
|
|
100
128
|
|
|
101
129
|
|
|
102
|
-
#
|
|
103
|
-
|
|
130
|
+
# (BIRD_SCHEMA_VERSION used to live here, hardcoded at "2.1.0" and read by
|
|
131
|
+
# nothing — a second, stale version constant. SCHEMA_VERSION above is the one
|
|
132
|
+
# the schema file and the migrations agree on.)
|
|
104
133
|
|
|
105
134
|
# Storage thresholds (per BIRD spec)
|
|
106
135
|
DEFAULT_INLINE_THRESHOLD = 4096 # 4KB - outputs smaller than this are stored inline
|
|
@@ -528,22 +557,23 @@ class BirdStore:
|
|
|
528
557
|
try:
|
|
529
558
|
parts = [int(p) for p in current_version.split(".")]
|
|
530
559
|
major, minor = parts[0], parts[1] if len(parts) > 1 else 0
|
|
531
|
-
if (major, minor) <
|
|
560
|
+
if (major, minor) < LATEST_SCHEMA:
|
|
532
561
|
return True
|
|
533
562
|
except (ValueError, IndexError):
|
|
534
563
|
return True # unparseable version — reconcile to be safe
|
|
535
564
|
# Self-heal for migrations that failed silently on dependent views and
|
|
536
565
|
# then let the version advance past themselves (leaving no column).
|
|
537
|
-
for table in (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
566
|
+
for table, required in REQUIRED_COLUMNS.items():
|
|
567
|
+
for column in required:
|
|
568
|
+
try:
|
|
569
|
+
has = conn.execute(
|
|
570
|
+
"SELECT 1 FROM information_schema.columns "
|
|
571
|
+
f"WHERE table_name = '{table}' AND column_name = '{column}'"
|
|
572
|
+
).fetchone()
|
|
573
|
+
if not has:
|
|
574
|
+
return True
|
|
575
|
+
except duckdb.Error:
|
|
544
576
|
return True
|
|
545
|
-
except duckdb.Error:
|
|
546
|
-
return True
|
|
547
577
|
return False
|
|
548
578
|
|
|
549
579
|
@staticmethod
|
|
@@ -553,8 +583,7 @@ class BirdStore:
|
|
|
553
583
|
to recreate them."""
|
|
554
584
|
try:
|
|
555
585
|
views = conn.execute(
|
|
556
|
-
"SELECT table_name FROM information_schema.tables "
|
|
557
|
-
"WHERE table_type = 'VIEW'"
|
|
586
|
+
"SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
|
|
558
587
|
).fetchall()
|
|
559
588
|
except duckdb.Error:
|
|
560
589
|
return
|
|
@@ -566,19 +595,19 @@ class BirdStore:
|
|
|
566
595
|
|
|
567
596
|
@classmethod
|
|
568
597
|
def _reconcile_schema(cls, conn: duckdb.DuckDBPyConnection) -> None:
|
|
569
|
-
"""Version-independent self-heal: ensure
|
|
570
|
-
|
|
571
|
-
by dependent views) and the schema version advanced past it.
|
|
572
|
-
already be dropped by the caller so the ALTER
|
|
573
|
-
|
|
574
|
-
Uses ADD COLUMN
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
data (from the 2.3 migration) is copied into the new
|
|
578
|
-
{"sandbox": ...}; the now-redundant `sandbox` column is
|
|
579
|
-
(harmless, and DROP COLUMN hits the same dependency wall).
|
|
598
|
+
"""Version-independent self-heal: ensure every column in REQUIRED_COLUMNS
|
|
599
|
+
exists, even if the migration that should have added it failed silently
|
|
600
|
+
(blocked by dependent views) and the schema version advanced past it.
|
|
601
|
+
Views must already be dropped by the caller so the ALTER can proceed.
|
|
602
|
+
|
|
603
|
+
Uses ADD COLUMN, NOT rename: RENAME COLUMN on these tables is blocked by
|
|
604
|
+
non-view dependencies (FKs/constraints) that persist even after dropping
|
|
605
|
+
views, whereas ADD COLUMN is never blocked. For `extension_data`,
|
|
606
|
+
existing `sandbox` data (from the 2.3 migration) is copied into the new
|
|
607
|
+
column wrapped as {"sandbox": ...}; the now-redundant `sandbox` column is
|
|
608
|
+
left in place (harmless, and DROP COLUMN hits the same dependency wall).
|
|
580
609
|
"""
|
|
581
|
-
for table in (
|
|
610
|
+
for table, required in REQUIRED_COLUMNS.items():
|
|
582
611
|
try:
|
|
583
612
|
cols = {
|
|
584
613
|
r[0]
|
|
@@ -587,21 +616,26 @@ class BirdStore:
|
|
|
587
616
|
f"WHERE table_name = '{table}'"
|
|
588
617
|
).fetchall()
|
|
589
618
|
}
|
|
590
|
-
if "extension_data" in cols:
|
|
591
|
-
continue
|
|
592
|
-
conn.execute(f"ALTER TABLE {table} ADD COLUMN extension_data JSON")
|
|
593
|
-
if "sandbox" in cols:
|
|
594
|
-
conn.execute(
|
|
595
|
-
f"UPDATE {table} SET extension_data = "
|
|
596
|
-
f"json_object('sandbox', sandbox) WHERE sandbox IS NOT NULL"
|
|
597
|
-
)
|
|
598
|
-
logger.info(
|
|
599
|
-
f"Repaired {table}: added extension_data (copied sandbox data)"
|
|
600
|
-
)
|
|
601
|
-
else:
|
|
602
|
-
logger.info(f"Repaired {table}: added extension_data")
|
|
603
619
|
except duckdb.Error as e:
|
|
604
620
|
logger.warning(f"Schema reconcile warning ({table}): {e}")
|
|
621
|
+
continue
|
|
622
|
+
if not cols:
|
|
623
|
+
continue # table doesn't exist yet; the schema file will create it
|
|
624
|
+
for column, column_type in required.items():
|
|
625
|
+
if column in cols:
|
|
626
|
+
continue
|
|
627
|
+
try:
|
|
628
|
+
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}")
|
|
629
|
+
if column == "extension_data" and "sandbox" in cols:
|
|
630
|
+
conn.execute(
|
|
631
|
+
f"UPDATE {table} SET extension_data = "
|
|
632
|
+
f"json_object('sandbox', sandbox) WHERE sandbox IS NOT NULL"
|
|
633
|
+
)
|
|
634
|
+
logger.info(f"Repaired {table}: added extension_data (copied sandbox data)")
|
|
635
|
+
else:
|
|
636
|
+
logger.info(f"Repaired {table}: added {column}")
|
|
637
|
+
except duckdb.Error as e:
|
|
638
|
+
logger.warning(f"Schema reconcile warning ({table}.{column}): {e}")
|
|
605
639
|
|
|
606
640
|
@classmethod
|
|
607
641
|
def _apply_migrations(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> None:
|
|
@@ -671,6 +705,16 @@ class BirdStore:
|
|
|
671
705
|
conn.execute("UPDATE blq_metadata SET value = '3.0.0' WHERE key = 'schema_version'")
|
|
672
706
|
logger.info("Migration: Updated schema version to 3.0.0 (BIRD directory)")
|
|
673
707
|
|
|
708
|
+
# Migration: 3.0.0 -> 3.1.0 (add events.log_content — duck_hunt's raw
|
|
709
|
+
# block for the event, previously received and dropped; see #52).
|
|
710
|
+
# _reconcile_schema does the ADD COLUMN and is idempotent, so it also
|
|
711
|
+
# covers a DB whose version advanced without the column landing.
|
|
712
|
+
if (major, minor) < (3, 1):
|
|
713
|
+
cls._reconcile_schema(conn)
|
|
714
|
+
migrations_applied = True
|
|
715
|
+
conn.execute("UPDATE blq_metadata SET value = '3.1.0' WHERE key = 'schema_version'")
|
|
716
|
+
logger.info("Migration: Added log_content column to events table")
|
|
717
|
+
|
|
674
718
|
# If migrations were applied, reload views/macros to pick up new columns
|
|
675
719
|
if migrations_applied:
|
|
676
720
|
cls._reload_views_and_macros(conn)
|
|
@@ -1583,6 +1627,10 @@ class BirdStore:
|
|
|
1583
1627
|
if not events:
|
|
1584
1628
|
return 0
|
|
1585
1629
|
|
|
1630
|
+
# Local import: core imports nothing from bird at module scope, and this
|
|
1631
|
+
# mirrors the existing lazy-import pattern used elsewhere in this module.
|
|
1632
|
+
from blq.commands.core import truncate_log_content
|
|
1633
|
+
|
|
1586
1634
|
date = datetime.now().strftime("%Y-%m-%d")
|
|
1587
1635
|
|
|
1588
1636
|
for idx, event in enumerate(events):
|
|
@@ -1594,10 +1642,10 @@ class BirdStore:
|
|
|
1594
1642
|
id, invocation_id, event_index, client_id, hostname,
|
|
1595
1643
|
event_type, severity, ref_file, ref_line, ref_column,
|
|
1596
1644
|
message, code, rule, tool_name, category, test_name,
|
|
1597
|
-
fingerprint, log_line_start, log_line_end,
|
|
1598
|
-
metadata, format_used, date
|
|
1645
|
+
fingerprint, log_line_start, log_line_end, log_content,
|
|
1646
|
+
context, metadata, format_used, date
|
|
1599
1647
|
)
|
|
1600
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1648
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1601
1649
|
""",
|
|
1602
1650
|
[
|
|
1603
1651
|
event_id,
|
|
@@ -1619,6 +1667,7 @@ class BirdStore:
|
|
|
1619
1667
|
event.get("fingerprint"),
|
|
1620
1668
|
event.get("log_line_start"),
|
|
1621
1669
|
event.get("log_line_end"),
|
|
1670
|
+
truncate_log_content(event.get("log_content")),
|
|
1622
1671
|
event.get("context"),
|
|
1623
1672
|
json.dumps(event.get("metadata")) if event.get("metadata") else None,
|
|
1624
1673
|
format_used,
|
|
@@ -26,7 +26,8 @@ CREATE TABLE IF NOT EXISTS blq_metadata (
|
|
|
26
26
|
);
|
|
27
27
|
|
|
28
28
|
-- Insert schema version (ignore if exists)
|
|
29
|
-
|
|
29
|
+
-- Keep in sync with SCHEMA_VERSION in bird.py (migrations key off it)
|
|
30
|
+
INSERT OR IGNORE INTO blq_metadata VALUES ('schema_version', '3.1.0');
|
|
30
31
|
INSERT OR IGNORE INTO blq_metadata VALUES ('storage_mode', 'duckdb');
|
|
31
32
|
|
|
32
33
|
-- Base path for blob storage (set at runtime)
|
|
@@ -230,6 +231,7 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
230
231
|
fingerprint VARCHAR, -- Unique identifier for dedup
|
|
231
232
|
log_line_start INTEGER, -- Start line in raw log
|
|
232
233
|
log_line_end INTEGER, -- End line in raw log
|
|
234
|
+
log_content VARCHAR, -- Raw log block for this event (duck_hunt log_content)
|
|
233
235
|
context VARCHAR, -- Surrounding context
|
|
234
236
|
metadata JSON, -- Format-specific extras
|
|
235
237
|
|
|
@@ -344,6 +346,7 @@ SELECT
|
|
|
344
346
|
e.fingerprint,
|
|
345
347
|
e.log_line_start,
|
|
346
348
|
e.log_line_end,
|
|
349
|
+
e.log_content,
|
|
347
350
|
e.context,
|
|
348
351
|
e.metadata,
|
|
349
352
|
|
|
@@ -11,6 +11,7 @@ import json
|
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
|
+
import shlex
|
|
14
15
|
import shutil
|
|
15
16
|
import subprocess
|
|
16
17
|
import sys
|
|
@@ -1320,15 +1321,21 @@ def expand_command(
|
|
|
1320
1321
|
raise ValueError(f"Missing required argument '{placeholder.name}'")
|
|
1321
1322
|
|
|
1322
1323
|
# Substitute placeholders in template
|
|
1324
|
+
# The expanded string is run with shell=True, so every value that comes from
|
|
1325
|
+
# a caller (placeholder values and passthrough/extra args) is shell-quoted to
|
|
1326
|
+
# prevent injection (e.g. `&& curl evil | sh`). The template itself is trusted
|
|
1327
|
+
# (defined at registration) and is left intact so it may carry intentional
|
|
1328
|
+
# shell syntax. Note: a placeholder value is now a single shell token — a
|
|
1329
|
+
# value like "-k foo" no longer splits into two arguments.
|
|
1323
1330
|
result = template
|
|
1324
1331
|
for match in _PLACEHOLDER_PATTERN.finditer(template):
|
|
1325
1332
|
name = match.group(1)
|
|
1326
|
-
result = result.replace(match.group(0), values[name], 1)
|
|
1333
|
+
result = result.replace(match.group(0), shlex.quote(values[name]), 1)
|
|
1327
1334
|
|
|
1328
|
-
# Append extra args
|
|
1335
|
+
# Append extra args (each quoted so passthrough args stay literal argv tokens)
|
|
1329
1336
|
all_extra = remaining_positional + (extra_args or [])
|
|
1330
1337
|
if all_extra:
|
|
1331
|
-
result = result + " " + " ".join(all_extra)
|
|
1338
|
+
result = result + " " + " ".join(shlex.quote(arg) for arg in all_extra)
|
|
1332
1339
|
|
|
1333
1340
|
return result
|
|
1334
1341
|
|
|
@@ -1860,6 +1867,37 @@ def get_next_run_id(lq_dir: Path) -> int:
|
|
|
1860
1867
|
return max_id + 1 if max_id > 0 else 1
|
|
1861
1868
|
|
|
1862
1869
|
|
|
1870
|
+
# ============================================================================
|
|
1871
|
+
# Event content
|
|
1872
|
+
# ============================================================================
|
|
1873
|
+
|
|
1874
|
+
# Cap on the stored `log_content` (duck_hunt's raw block for an event, e.g. the
|
|
1875
|
+
# whole pytest FAILURES block). It is bounded in practice — the block, not the
|
|
1876
|
+
# log — but not in principle: a parser that spans a large region, or a test that
|
|
1877
|
+
# dumps a large diff, can produce a very big value, and every event of a run
|
|
1878
|
+
# would carry its own copy. 32K is well above any realistic traceback while
|
|
1879
|
+
# keeping a pathological run from bloating storage.
|
|
1880
|
+
LOG_CONTENT_MAX_CHARS = 32_768
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
def truncate_log_content(value: str | None, max_chars: int = LOG_CONTENT_MAX_CHARS) -> str | None:
|
|
1884
|
+
"""Cap `log_content`, making any truncation visible in the value itself.
|
|
1885
|
+
|
|
1886
|
+
Silent shortening has bitten this codebase before, so an over-long value
|
|
1887
|
+
keeps its head and gains a marker stating what was kept and what existed:
|
|
1888
|
+
|
|
1889
|
+
... [blq: log_content truncated to 32768 of 91234 chars]
|
|
1890
|
+
|
|
1891
|
+
Values at or under the cap are returned unchanged (including None/"").
|
|
1892
|
+
"""
|
|
1893
|
+
if not value or len(value) <= max_chars:
|
|
1894
|
+
return value
|
|
1895
|
+
return (
|
|
1896
|
+
value[:max_chars]
|
|
1897
|
+
+ f"\n... [blq: log_content truncated to {max_chars} of {len(value)} chars]"
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
|
|
1863
1901
|
# ============================================================================
|
|
1864
1902
|
# Parquet Writing
|
|
1865
1903
|
# ============================================================================
|
|
@@ -1901,7 +1939,11 @@ PARQUET_SCHEMA = [
|
|
|
1901
1939
|
("ref_column", "BIGINT"),
|
|
1902
1940
|
# Content
|
|
1903
1941
|
("message", "VARCHAR"),
|
|
1904
|
-
(
|
|
1942
|
+
# duck_hunt's raw block for this event (e.g. the whole pytest FAILURES
|
|
1943
|
+
# block). Replaces the vestigial `raw_text`, which nothing ever populated —
|
|
1944
|
+
# duck_hunt's field has been named log_content since its Schema V3
|
|
1945
|
+
# (see docs/design/duck-hunt-v3-migration.md).
|
|
1946
|
+
("log_content", "VARCHAR"),
|
|
1905
1947
|
# Classification
|
|
1906
1948
|
("tool_name", "VARCHAR"),
|
|
1907
1949
|
("category", "VARCHAR"),
|
|
@@ -1974,6 +2016,8 @@ def write_run_parquet(
|
|
|
1974
2016
|
for event in events or [{}]:
|
|
1975
2017
|
# Merge run metadata and event data
|
|
1976
2018
|
merged = {**run_meta, **event}
|
|
2019
|
+
if merged.get("log_content"):
|
|
2020
|
+
merged["log_content"] = truncate_log_content(merged["log_content"])
|
|
1977
2021
|
# Build row with all columns in schema order
|
|
1978
2022
|
vals = []
|
|
1979
2023
|
for col in PARQUET_SCHEMA_COLUMNS:
|
|
@@ -45,7 +45,7 @@ from blq.commands.core import (
|
|
|
45
45
|
parse_log_content,
|
|
46
46
|
write_run_parquet,
|
|
47
47
|
)
|
|
48
|
-
from blq.ext import CommandSpec
|
|
48
|
+
from blq.ext import CommandSpec, capture_env
|
|
49
49
|
from blq.ext.discovery import load_extensions, order_extensions
|
|
50
50
|
from blq.ext.local_executor import LocalExecutor
|
|
51
51
|
from blq.ext.pipeline import run_pipeline
|
|
@@ -772,6 +772,7 @@ def _execute_command(
|
|
|
772
772
|
stderr=subprocess.STDOUT,
|
|
773
773
|
text=True,
|
|
774
774
|
start_new_session=True,
|
|
775
|
+
env=capture_env(),
|
|
775
776
|
)
|
|
776
777
|
|
|
777
778
|
output_lines: list[str] = []
|
|
@@ -1137,6 +1138,7 @@ def _run_no_capture(command: str, quiet: bool = False) -> int:
|
|
|
1137
1138
|
stderr=subprocess.STDOUT,
|
|
1138
1139
|
text=True,
|
|
1139
1140
|
start_new_session=True,
|
|
1141
|
+
env=capture_env(),
|
|
1140
1142
|
)
|
|
1141
1143
|
|
|
1142
1144
|
assert process.stdout is not None # stdout=PIPE ensures this
|
|
@@ -1559,6 +1561,39 @@ def cmd_exec(args: argparse.Namespace) -> None:
|
|
|
1559
1561
|
sys.exit(result.exit_code)
|
|
1560
1562
|
|
|
1561
1563
|
|
|
1564
|
+
def _write_imported_run(
|
|
1565
|
+
events: list[dict[str, Any]],
|
|
1566
|
+
run_meta: dict[str, Any],
|
|
1567
|
+
config: "BlqConfig",
|
|
1568
|
+
lq_dir: Path,
|
|
1569
|
+
) -> Path:
|
|
1570
|
+
"""Persist an imported/captured run to the ACTIVE storage backend.
|
|
1571
|
+
|
|
1572
|
+
`import` and `capture` used to call `write_run_parquet` unconditionally,
|
|
1573
|
+
while `exec`/`run` branch on `config.use_bird`. BIRD is the default storage
|
|
1574
|
+
mode, so on a default project those two commands wrote a well-formed
|
|
1575
|
+
parquet under `.bird/logs/` that the query layer — a view over the BIRD
|
|
1576
|
+
tables — never reads:
|
|
1577
|
+
|
|
1578
|
+
$ blq import report.json --format pytest_json
|
|
1579
|
+
Imported 2 events (2 errors, 0 warnings)
|
|
1580
|
+
Saved to .bird/logs/date=.../source=import/001_report.parquet
|
|
1581
|
+
$ blq events
|
|
1582
|
+
(no data)
|
|
1583
|
+
|
|
1584
|
+
Success, a destination path, exit 0, a real file, and nothing observable.
|
|
1585
|
+
The branch was added to the exec path and never to these two.
|
|
1586
|
+
|
|
1587
|
+
Raw output is deliberately not stored here: neither command previously
|
|
1588
|
+
retained it, and this function is fixing where events land, not what else
|
|
1589
|
+
gets kept.
|
|
1590
|
+
"""
|
|
1591
|
+
if config.use_bird:
|
|
1592
|
+
_invocation_id, db_path = write_bird_invocation(events, run_meta, lq_dir)
|
|
1593
|
+
return db_path
|
|
1594
|
+
return write_run_parquet(events, run_meta, lq_dir)
|
|
1595
|
+
|
|
1596
|
+
|
|
1562
1597
|
def cmd_import(args: argparse.Namespace) -> None:
|
|
1563
1598
|
"""Import an existing log file."""
|
|
1564
1599
|
config = BlqConfig.ensure()
|
|
@@ -1587,7 +1622,7 @@ def cmd_import(args: argparse.Namespace) -> None:
|
|
|
1587
1622
|
"exit_code": 0,
|
|
1588
1623
|
}
|
|
1589
1624
|
|
|
1590
|
-
outpath =
|
|
1625
|
+
outpath = _write_imported_run(events, run_meta, config, lq_dir)
|
|
1591
1626
|
|
|
1592
1627
|
errors = sum(1 for e in events if e.get("severity") == "error")
|
|
1593
1628
|
warnings = sum(1 for e in events if e.get("severity") == "warning")
|
|
@@ -1620,7 +1655,7 @@ def cmd_capture(args: argparse.Namespace) -> None:
|
|
|
1620
1655
|
"exit_code": 0,
|
|
1621
1656
|
}
|
|
1622
1657
|
|
|
1623
|
-
outpath =
|
|
1658
|
+
outpath = _write_imported_run(events, run_meta, config, lq_dir)
|
|
1624
1659
|
|
|
1625
1660
|
errors = sum(1 for e in events if e.get("severity") == "error")
|
|
1626
1661
|
warnings = sum(1 for e in events if e.get("severity") == "warning")
|
|
@@ -11,6 +11,8 @@ from datetime import datetime
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Protocol
|
|
13
13
|
|
|
14
|
+
import os
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
@dataclass
|
|
16
18
|
class CommandSpec:
|
|
@@ -83,3 +85,32 @@ class Executor(Protocol):
|
|
|
83
85
|
name: str
|
|
84
86
|
|
|
85
87
|
def execute(self, spec: CommandSpec) -> ExecutionResult: ...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
#: Terminal width presented to captured child processes. Wide enough that the
|
|
91
|
+
#: tools we parse stop abbreviating; not so wide that a tool wrapping to it
|
|
92
|
+
#: produces unreadable raw logs.
|
|
93
|
+
CAPTURE_COLUMNS = 200
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def capture_env(base: dict[str, str] | None = None) -> dict[str, str]:
|
|
97
|
+
"""Environment for a captured child process.
|
|
98
|
+
|
|
99
|
+
Anything that formats for a terminal consults COLUMNS (or an ioctl) and
|
|
100
|
+
falls back to 80 columns when there is no TTY — which is always, under a
|
|
101
|
+
capturing runner. `pytest -q` abbreviates its short-summary line on that
|
|
102
|
+
basis, and since blq parses that line, the stored message loses everything
|
|
103
|
+
past roughly 80 columns minus the length of the test name:
|
|
104
|
+
|
|
105
|
+
73-char test name -> 7 characters survive ('asse...')
|
|
106
|
+
35-char test name -> 45 characters survive, nothing is lost
|
|
107
|
+
|
|
108
|
+
So how diagnosable a failure was depended on how long someone had named
|
|
109
|
+
the test. Presenting a wide COLUMNS fixes the whole class at the point of
|
|
110
|
+
capture — no parser can recover text the tool never printed.
|
|
111
|
+
|
|
112
|
+
An explicit COLUMNS from the caller wins: someone who set it meant it.
|
|
113
|
+
"""
|
|
114
|
+
env = dict(os.environ if base is None else base)
|
|
115
|
+
env.setdefault("COLUMNS", str(CAPTURE_COLUMNS))
|
|
116
|
+
return env
|
|
@@ -14,7 +14,7 @@ from datetime import datetime
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from types import FrameType
|
|
16
16
|
|
|
17
|
-
from blq.ext import CommandSpec, ExecutionResult
|
|
17
|
+
from blq.ext import CommandSpec, ExecutionResult, capture_env
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger("blq-ext")
|
|
20
20
|
|
|
@@ -78,7 +78,9 @@ class LocalExecutor:
|
|
|
78
78
|
signal.signal(signal.SIGINT, _cleanup)
|
|
79
79
|
signal.signal(signal.SIGTERM, _cleanup)
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
# Always via capture_env: a captured child has no TTY, so
|
|
82
|
+
# terminal-formatting tools self-truncate at 80 columns.
|
|
83
|
+
run_env = capture_env({**os.environ, **spec.env} if spec.env else None)
|
|
82
84
|
process = subprocess.Popen(
|
|
83
85
|
spec.command,
|
|
84
86
|
shell=True,
|