graphite-code 0.3.0__tar.gz → 0.4.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.
- {graphite_code-0.3.0 → graphite_code-0.4.0}/CHANGELOG.md +78 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/PKG-INFO +1 -1
- {graphite_code-0.3.0 → graphite_code-0.4.0}/RELEASING.md +46 -13
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/__init__.py +1 -1
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/doctor_probes.py +38 -4
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/extract/ast.py +125 -1
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle_storage.py +22 -8
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/storage.py +30 -12
- graphite_code-0.4.0/tests/data/sqloracle/blindspots.py.txt +206 -0
- graphite_code-0.4.0/tests/data/sqloracle/round69.py.txt +79 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor.py +284 -7
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_graph_provider_isolation.py +15 -7
- graphite_code-0.4.0/tests/test_method_dispatch_scope.py +290 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_probe_workspace.py +12 -1
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_python_resolver.py +96 -1
- graphite_code-0.4.0/tests/test_sqloracle.py +284 -0
- graphite_code-0.4.0/tests/test_verify_published_release.py +409 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/.gitignore +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/ARCHITECTURE.md +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/CONTRIBUTING.md +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/LICENSE +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/README.md +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/pyproject.toml +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/__main__.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/_cleanup_worker.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/activation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/agent_hooks.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/agent_settings.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/analyze.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/answer_contract.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/bootstrap.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/buildlock.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/cache.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/channel.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/cli.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/cluster.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/config.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/context.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/daemon.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/daemon_health.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/debt.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/dependency_install.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/detach.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/doctor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/engine_identity.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/__init__.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/html.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/json.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/md.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/extract/__init__.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/freshness.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/git.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/graph.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/graph_io.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/health.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/hook_entry.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/hookinstall.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/hookshim.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/incident_ledger.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/ingest.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/init.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/io.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/listing.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/llm.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/llm_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/mcp.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/mcp_server.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/natural_query.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/overlays.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/probe_process.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/probe_workspace.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/process_contracts.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/provider_observer.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/query.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/query_plan.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/replacement_audit.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/resolve.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/review.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/__init__.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/approval.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/classifier.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/claude_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/claude_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/cli_identity.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/codex_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/codex_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/context_builder.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/contracts.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/diff_policy.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/edit_apply.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/effort.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle_operator.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle_service.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/ollama_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/ollama_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/openrouter_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/openrouter_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/policy.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/probe_runner.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/process_runner.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/profiles.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/prompt.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/registry.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/route_pool.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/route_pool_execution.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/schema_validation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/service.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/settings.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/shadow.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/telemetry.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/worktree.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/zai_edit.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/zai_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/zai_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/savings.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/ts_bridge.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/ts_resolver.mjs +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/typescript_activation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/usage_ledger.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/validation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/watch.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/windows_job.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/windows_startup.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/windows_task.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/conftest.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fake_clis/fake_cli.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/provider_lifecycle_schema_v1.sql +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/routing_schema_v2_ca77600.sql +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/routing_schema_v3_94eb333.sql +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/routing_schema_v4_lifecycle_migration.sql +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_activation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_activation_backstop.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_agent_hook_activation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_agent_hooks.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_agent_settings.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_answer_contract.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_arrow_and_constructor_binding.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_bootstrap.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_build_detach.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_buildlock.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cache_engine_identity.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cache_file_set.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cache_pruning.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_call_graph.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_cli.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_core.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_lock.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_mcp.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_register.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_report.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_status.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_class_field_arrow_binding.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cli_channel.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cli_version.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_context.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cwd_relative_config.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_activation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_build_lock.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_engine_staleness.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_health.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_health_activation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_debt.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_destructured_binding.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_determinism.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor_agent_hooks.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor_hooks.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor_managed_docs.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_documentation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_edit_apply.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_engine_identity.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_external_calls.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_git_security.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_go_rust.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_graph_io.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hardening.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_health.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hook_entry.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hook_template.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hookinstall.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hookshim.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_html_security.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_incident_ledger.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init_activation_doctrine.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init_hooks.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init_validation_exit.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_lifecycle_operator.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_listing.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_listing_surfaces.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_llm.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_mcp.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_monorepo.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_natural_query.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_overlays.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_path_leak_fixture.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_probe_diagnostics.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_claude_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_codex_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_lifecycle.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_lifecycle_service.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_lifecycle_storage.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_observer.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_ollama_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_openrouter_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_probe_runner.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_public_surface.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_published_schemas.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_query_plan.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_reliability.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_replacement_audit.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_resolve.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_review.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_route_pool.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_approval.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_benchmark.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_classifier.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_claude_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_cli.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_cli_contracts.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_cli_recovery.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_codex_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_context.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_contracts.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_diff_policy.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_openrouter_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_policy.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_process_runner.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_profiles.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_registry.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_schema_validation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_security.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_service.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_shadow.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_storage.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_telemetry.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_worktree.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_zai_executor.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_zai_probe.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_savings.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_search.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_smoke.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_typescript_activation.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_typescript_resolver.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_usage_ledger.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_verify_artifact.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_watch.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_windows_startup.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_windows_task.py +0 -0
- {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_zai_edit.py +0 -0
|
@@ -12,6 +12,84 @@ machine-checkable identity; the version is for humans.
|
|
|
12
12
|
|
|
13
13
|
[Keep a Changelog]: https://keepachangelog.com/en/1.1.0/
|
|
14
14
|
|
|
15
|
+
## [0.4.0] — 2026-08-14
|
|
16
|
+
|
|
17
|
+
**A minor bump because consumer graphs change, not because an API did.** Both fixes
|
|
18
|
+
below remove or add graph edges, so every consumer that rebuilds will see different
|
|
19
|
+
`callers`/`context` answers and a slightly different Python `calls` ratio. Nothing in
|
|
20
|
+
the CLI or the JSON schema changed. Under this project's pre-1.0 rule — minor versions
|
|
21
|
+
may break things — a patch number would have understated a release that silently
|
|
22
|
+
changes what the graph says.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
**Python method calls bound to any same-named definition in the repo, however
|
|
27
|
+
unreachable** (#54). `_resolve_method_dispatch` re-pointed `recv.method()` to any
|
|
28
|
+
`is_method` node sharing the name, justified in its own docstring by "method names
|
|
29
|
+
are almost always globally unique" — true of domain names, false of every name a
|
|
30
|
+
standard library already owns. On graphite's own graph `query "callers resolve"`
|
|
31
|
+
returned **232 callers graded `decision_grade`**, every one an ordinary
|
|
32
|
+
`pathlib.Path(...).resolve()` re-pointed to a test double's `resolve` in
|
|
33
|
+
`tests/test_git_security.py`; `src/graphite/io.py::atomic_write_text` was recorded
|
|
34
|
+
as calling a `write` defined in `tests/test_doctor.py`. The mis-bindings counted as
|
|
35
|
+
`bound`, so the health ratio rose as the defect got worse.
|
|
36
|
+
|
|
37
|
+
A Python call now dispatches only to a definition in the calling file or in a file
|
|
38
|
+
it directly imports. Filtering happens **before** the ambiguity cap, because
|
|
39
|
+
reachability is the disambiguation that cap was standing in for — so a common name
|
|
40
|
+
with one reachable definition now resolves instead of being abandoned as "too
|
|
41
|
+
generic to guess".
|
|
42
|
+
|
|
43
|
+
Measured on this repo: `callers resolve` 232 → 5, `callers get` 87 → 0,
|
|
44
|
+
`callers write` 30 → 0; `calls` edges 13498 → 12497 (−1428, +427); `calls.python`
|
|
45
|
+
0.980 → 0.977. Every one of the 1428 dropped edges buckets as
|
|
46
|
+
`src|scripts|tests → tests` or `src → src`, and every sampled member was another
|
|
47
|
+
false binding (`sqlite3.connect`, `Thread.start`, a file handle's `write`).
|
|
48
|
+
|
|
49
|
+
**Expect consumer graphs to lose call edges and report a slightly lower Python
|
|
50
|
+
`calls` ratio.** That is honest accounting, not a regression — the same shape as the
|
|
51
|
+
constructor-edge change that moved some javascript cells down. Do not "fix" it back.
|
|
52
|
+
|
|
53
|
+
The gate resolves language through `LANGUAGE_BY_EXT`, the same table `collect_files`
|
|
54
|
+
uses to choose an extractor, so it cannot disagree with the walk that produced the
|
|
55
|
+
edge. Verified to reach a **warm** cache too: a graph built by the old code, rebuilt
|
|
56
|
+
in place by the new code, loses the false binding — the dispatch pass runs after
|
|
57
|
+
`cache.write`, so a stale partition cannot preserve it.
|
|
58
|
+
|
|
59
|
+
Scoped to **Python only, on evidence**. The gate's premise — "the caller does not
|
|
60
|
+
import the definer" standing in for "the caller cannot be holding one" — is never
|
|
61
|
+
exact in a duck-typed language. TypeScript/JavaScript stay ungated because
|
|
62
|
+
`test_member_call_ambiguous_small_set_links_to_all` pins the documented fan-out on
|
|
63
|
+
an `x: any` receiver with no import at all, a shape the gate would delete; Rust
|
|
64
|
+
because this repo has none to measure. Go could not be gated at all: its in-repo
|
|
65
|
+
imports target a synthesized package id that never equals any file's node id.
|
|
66
|
+
Residual: the same defect survives in ungated JS/TS.
|
|
67
|
+
|
|
68
|
+
**`import pkg.sub` emitted no import edge to `pkg/__init__.py`** (#55), though that
|
|
69
|
+
statement binds the name `pkg` and executes the package. Every dotted importer was
|
|
70
|
+
therefore invisible as a dependent of that `__init__.py` — the file where re-exports
|
|
71
|
+
and shared constants live, so a high-traffic edit target whose blast radius was
|
|
72
|
+
understated. Silently: a missing edge cannot lower the imports ratio, which is
|
|
73
|
+
computed over the edges that *were* emitted.
|
|
74
|
+
|
|
75
|
+
Found because the #54 gate refused `pkg.build()` and a test caught it. The gate
|
|
76
|
+
initially compensated with its own ancestor expansion; with the real edges emitted
|
|
77
|
+
that compensation became unfalsifiable — removing it changed no test — so it was
|
|
78
|
+
deleted rather than left looking like protection.
|
|
79
|
+
`test_a_dotted_import_reaches_the_package_it_binds` now guards the coupling.
|
|
80
|
+
|
|
81
|
+
Measured on this repo: import edges 1890 → 1920 (+30, −0), imports health 740/740 →
|
|
82
|
+
770/770 with `ratio` 1.0 and `external` **unchanged** at 1148 — purely additive, no
|
|
83
|
+
reclassification. `impact src/graphite/routing/__init__.py` went from 1 likely test
|
|
84
|
+
to **8**.
|
|
85
|
+
|
|
86
|
+
Scoped to `import a.b.c`, not `from a.b import c`. Both execute the package, but only
|
|
87
|
+
the first binds `a`, so only the first can carry a `a.something()` call to attribute;
|
|
88
|
+
the from-import spelling has two existing specifications pinning one edge per module,
|
|
89
|
+
and expanding it would have added ~234 further edges here with no measured demand.
|
|
90
|
+
Ancestors are edged only when they resolve in-repo, so a PEP 420 namespace package
|
|
91
|
+
(no `__init__.py`) and an external `import os.path` both emit nothing extra.
|
|
92
|
+
|
|
15
93
|
## [0.3.0] — 2026-08-12
|
|
16
94
|
|
|
17
95
|
**The first release consumers install as a built wheel.** Through 0.2.1 every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: graphite-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Local-first, zero-LLM knowledge graph extraction for codebases.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jared0565/graphite
|
|
6
6
|
Project-URL: Repository, https://github.com/jared0565/graphite
|
|
@@ -72,12 +72,12 @@ not a harmless duplicate: Hatchling refuses to build a field declared both stati
|
|
|
72
72
|
and dynamically, so the next build aborts in `_get_version` rather than producing a
|
|
73
73
|
mismatched artifact.
|
|
74
74
|
|
|
75
|
-
The single source is deliberate.
|
|
76
|
-
install,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
The single source is deliberate. `importlib.metadata` reports whatever was written into
|
|
76
|
+
the installed metadata at install time, so a version set only in `pyproject.toml` reaches
|
|
77
|
+
a consumer only when someone reinstalls. `graphite --version` reads
|
|
78
|
+
`graphite.__version__` for that reason, and reports a `stale-install` line when the
|
|
79
|
+
installed metadata disagrees — which is how a development checkout and an installed
|
|
80
|
+
build are told apart at all.
|
|
81
81
|
|
|
82
82
|
Review the complete diff and confirm no unrelated changes exist, and that
|
|
83
83
|
`pyproject.toml` still declares the version dynamic:
|
|
@@ -275,8 +275,34 @@ git push origin vVERSION
|
|
|
275
275
|
|
|
276
276
|
Never force push. Publication to PyPI or another index may use only a separately approved
|
|
277
277
|
mechanism from a secure environment with scoped credentials. Never place tokens in
|
|
278
|
-
command-line arguments, shell history, repository files, or logs.
|
|
279
|
-
|
|
278
|
+
command-line arguments, shell history, repository files, or logs.
|
|
279
|
+
|
|
280
|
+
**An index IS configured as of 2026-08-14: PyPI, distribution `graphite-code`.**
|
|
281
|
+
Publication runs from `.github/workflows/publish.yml` by Trusted Publishing (OIDC),
|
|
282
|
+
`workflow_dispatch` only. There is no API token in this repository, in any secret, or on
|
|
283
|
+
any maintainer machine — the credential is minted per run and expires in seconds, which
|
|
284
|
+
satisfies the paragraph above by having no long-lived secret to mishandle. The workflow
|
|
285
|
+
publishes the artifacts already attached to the GitHub Release and verifies both SHA256s
|
|
286
|
+
against literals pinned in the reviewed workflow; it never builds. Releasing a new version
|
|
287
|
+
therefore requires editing the approved version and both digests in that file and
|
|
288
|
+
committing the change, so digests pass through review rather than a dispatch form.
|
|
289
|
+
|
|
290
|
+
After publishing, verify from the INDEX rather than from this checkout:
|
|
291
|
+
|
|
292
|
+
```text
|
|
293
|
+
python scripts/verify_published_release.py VERSION --wheel-sha256 <from EVIDENCE.md>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Run it BEFORE publishing as well. Every arm must fail while the version does not exist —
|
|
297
|
+
that negative control is what makes the throwaway venv's isolation from this checkout a
|
|
298
|
+
measured fact instead of an assumption.
|
|
299
|
+
|
|
300
|
+
The digest is the load-bearing arm and the only one that may be omitted. Omitting it is
|
|
301
|
+
legal, and the run then reports `1 arm(s) never ran` and exits 0 having compared no bytes
|
|
302
|
+
— read the arm counts, not just `OVERALL`. An expected digest that is present but is not
|
|
303
|
+
64 hex characters is a FAIL that says `malformed`, which is a different diagnosis from a
|
|
304
|
+
substituted wheel. `tests/test_verify_published_release.py` holds every arm to this,
|
|
305
|
+
including the ones that only fire on a bad release.
|
|
280
306
|
|
|
281
307
|
## Retention and rollback
|
|
282
308
|
|
|
@@ -289,11 +315,18 @@ when a rollback is wanted. The store lives at
|
|
|
289
315
|
rather than as a literal: this file ships inside the sdist, so an absolute path
|
|
290
316
|
here is the maintainer's layout published as if it were policy.
|
|
291
317
|
|
|
292
|
-
This is not bookkeeping.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
318
|
+
This is not bookkeeping. Rollback means "reinstall the last known-good wheel",
|
|
319
|
+
and that is possible only for a wheel somebody kept. A build directed at a
|
|
320
|
+
scratch directory satisfies the gate above and leaves the gap open.
|
|
321
|
+
|
|
322
|
+
Through 0.2.1 consumers imported Graphite from one shared editable install
|
|
323
|
+
pointed at the development tree, so a saved file was live for all of them with
|
|
324
|
+
no build and no boundary. **That stopped being true at 0.3.0**, which ships as a
|
|
325
|
+
built wheel: `graphite.__file__` resolves under `site-packages` and no editable
|
|
326
|
+
`.pth` survives. The retention rule did not weaken — it got sharper. A fix now
|
|
327
|
+
reaches nobody until a release is cut, so the store is the only record of what
|
|
328
|
+
consumers are actually running, and `index.md`'s "Currently deployed" section is
|
|
329
|
+
the only place that distinguishes it from what is on `main`.
|
|
297
330
|
|
|
298
331
|
Rolling back, in an environment that already has the runtime dependencies:
|
|
299
332
|
|
|
@@ -38,4 +38,4 @@ __all__ = ["__version__"]
|
|
|
38
38
|
#: Coarse and hand-maintained by construction. It is a release label, not
|
|
39
39
|
#: evidence that a given fix is present -- to answer that, survey for the marker
|
|
40
40
|
#: the fix introduced, or compare `graphite --version` fingerprints.
|
|
41
|
-
__version__ = "0.
|
|
41
|
+
__version__ = "0.4.0"
|
|
@@ -10,7 +10,7 @@ import sys
|
|
|
10
10
|
import tempfile
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
|
-
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Callable, Mapping
|
|
14
14
|
from concurrent.futures import ThreadPoolExecutor
|
|
15
15
|
from functools import lru_cache
|
|
16
16
|
from importlib import machinery, metadata
|
|
@@ -490,13 +490,36 @@ _TYPESCRIPT_REMEDIATION = (
|
|
|
490
490
|
)
|
|
491
491
|
|
|
492
492
|
|
|
493
|
-
def _blocked(error_type: str, code: str) -> DoctorCheck:
|
|
493
|
+
def _blocked(error_type: str, code: str, *, masked: Mapping[str, object] | None = None) -> DoctorCheck:
|
|
494
|
+
"""Report one failure, optionally carrying the cause it displaced.
|
|
495
|
+
|
|
496
|
+
`masked` is for the one place a result legitimately overrides another: a
|
|
497
|
+
cleanup failure outranks the primary error, because leaked state is more
|
|
498
|
+
urgent than a reproducible one. Overriding it must not ERASE it -- reporting
|
|
499
|
+
only `cleanup_timeout` for a pipeline that exited nonzero sends an operator
|
|
500
|
+
to temp directories instead of to the build (#53).
|
|
501
|
+
|
|
502
|
+
Both carried values come from a fixed vocabulary produced by
|
|
503
|
+
`_process_error_type` and this module's own literals, so neither can carry a
|
|
504
|
+
path. That is why they are copied by NAME rather than by merging the
|
|
505
|
+
displaced details wholesale -- a future detail key holding a path would
|
|
506
|
+
otherwise be promoted into this report for free.
|
|
507
|
+
"""
|
|
508
|
+
details: dict[str, object] = {"error_type": error_type, "code": code}
|
|
509
|
+
if masked is not None:
|
|
510
|
+
masked_type = masked.get("error_type")
|
|
511
|
+
masked_code = masked.get("code")
|
|
512
|
+
# Absent, never null: `masked_error_type: null` would invite a reader to
|
|
513
|
+
# treat "nothing was displaced" as "something was, and it was nothing".
|
|
514
|
+
if isinstance(masked_type, str) and isinstance(masked_code, str):
|
|
515
|
+
details["masked_error_type"] = masked_type
|
|
516
|
+
details["masked_code"] = masked_code
|
|
494
517
|
return DoctorCheck(
|
|
495
518
|
"deep_core",
|
|
496
519
|
"Deterministic pipeline",
|
|
497
520
|
"blocked",
|
|
498
521
|
"The isolated deterministic pipeline probe failed safely.",
|
|
499
|
-
|
|
522
|
+
details,
|
|
500
523
|
("Run the Graphite build, validate, and query commands locally and inspect their diagnostics.",),
|
|
501
524
|
)
|
|
502
525
|
|
|
@@ -735,7 +758,18 @@ def probe_core_pipeline(
|
|
|
735
758
|
if lease is not None:
|
|
736
759
|
cleanup_error = _cleanup_workspace_lease(lease, deadline, _clock)
|
|
737
760
|
if cleanup_error is not None:
|
|
738
|
-
|
|
761
|
+
# Passed unconditionally: `_blocked` carries a cause only when
|
|
762
|
+
# one is actually present. A probe that SUCCEEDED and then failed
|
|
763
|
+
# to tear down has details with no `error_type`, so nothing is
|
|
764
|
+
# carried -- which is the outcome we want.
|
|
765
|
+
#
|
|
766
|
+
# An earlier version also tested `candidate.status == "blocked"`
|
|
767
|
+
# here. It was removed because it could not be shown to guard: it
|
|
768
|
+
# made `masked` unreachable for a ready candidate, which in turn
|
|
769
|
+
# made the type check inside `_blocked` unreachable, so each
|
|
770
|
+
# guard's only effect was to hide the other from every test.
|
|
771
|
+
displaced = candidate.details if candidate is not None else None
|
|
772
|
+
candidate = _blocked("cleanup", cleanup_error, masked=displaced)
|
|
739
773
|
else:
|
|
740
774
|
_release_core_probe_slot()
|
|
741
775
|
|
|
@@ -11,7 +11,7 @@ from typing import Any, Collection, Final
|
|
|
11
11
|
|
|
12
12
|
from ..cache import Cache
|
|
13
13
|
from ..config import Config
|
|
14
|
-
from ..ingest import FileEntry
|
|
14
|
+
from ..ingest import LANGUAGE_BY_EXT, FileEntry
|
|
15
15
|
from ..resolve import SourceIndex, should_keep_call_target
|
|
16
16
|
|
|
17
17
|
|
|
@@ -1096,6 +1096,37 @@ def _python_import_modules(node: Any) -> list[tuple[str, int]]:
|
|
|
1096
1096
|
return out
|
|
1097
1097
|
|
|
1098
1098
|
|
|
1099
|
+
def _python_ancestor_packages(
|
|
1100
|
+
module: str, dots: int, rel_path: str, source_index: SourceIndex | None
|
|
1101
|
+
) -> list[str]:
|
|
1102
|
+
"""Resolved paths of the in-repo packages `import module` also executes (#55).
|
|
1103
|
+
|
|
1104
|
+
`import pkg.sub` imports `pkg` as well: Python executes `pkg/__init__.py` and
|
|
1105
|
+
binds the ROOT name in the importer, so `pkg.build()` afterwards is a call into
|
|
1106
|
+
the package. Recording only the deepest module made every dotted importer
|
|
1107
|
+
invisible as a dependent of that `__init__.py` -- and a package `__init__.py` is
|
|
1108
|
+
where re-exports and shared constants live, so `impact` understated the blast
|
|
1109
|
+
radius of a high-traffic edit target. Silently: a missing edge cannot lower the
|
|
1110
|
+
imports ratio, which is computed over the edges that WERE emitted.
|
|
1111
|
+
|
|
1112
|
+
Scoped to ancestors that resolve IN-REPO. `import os.path` gains nothing from a
|
|
1113
|
+
second edge to `os`: it would inflate the external count and move the imports
|
|
1114
|
+
ratio for a package that can never be a blast-radius target.
|
|
1115
|
+
|
|
1116
|
+
The counterpart for the other spelling is `_python_from_import_submodules`,
|
|
1117
|
+
added for the mirror-image defect in issue #7.
|
|
1118
|
+
"""
|
|
1119
|
+
if source_index is None or "." not in module:
|
|
1120
|
+
return []
|
|
1121
|
+
segments = module.split(".")
|
|
1122
|
+
out: list[str] = []
|
|
1123
|
+
for depth in range(1, len(segments)):
|
|
1124
|
+
resolved = source_index.resolve_python_module(rel_path, ".".join(segments[:depth]), dots)
|
|
1125
|
+
if resolved:
|
|
1126
|
+
out.append(resolved)
|
|
1127
|
+
return out
|
|
1128
|
+
|
|
1129
|
+
|
|
1099
1130
|
def _python_from_import_submodules(
|
|
1100
1131
|
node: Any, rel_path: str, source_index: SourceIndex | None
|
|
1101
1132
|
) -> list[str]:
|
|
@@ -1326,6 +1357,19 @@ def _extract_python(file_id: str, rel_path: str, _source: bytes, tree: Any, sour
|
|
|
1326
1357
|
file_id, _file_node_id(resolved), "imports", rel_path,
|
|
1327
1358
|
_line(node), confidence="EXACT_IMPORT",
|
|
1328
1359
|
))
|
|
1360
|
+
# `import pkg.sub` also imports `pkg` (#55). Deliberately NOT
|
|
1361
|
+
# applied to `from pkg.sub import x`: that executes the package
|
|
1362
|
+
# too, but binds only `x`, so there is no `pkg.` call to
|
|
1363
|
+
# attribute -- and `test_from_import_symbol_only_edge_unchanged`
|
|
1364
|
+
# pins one edge per module for that spelling.
|
|
1365
|
+
for ancestor in (
|
|
1366
|
+
_python_ancestor_packages(module, dots, rel_path, source_index)
|
|
1367
|
+
if node.type == "import_statement" else ()
|
|
1368
|
+
):
|
|
1369
|
+
result.edges.append(_edge(
|
|
1370
|
+
file_id, _file_node_id(ancestor), "imports", rel_path,
|
|
1371
|
+
_line(node), confidence="EXACT_IMPORT",
|
|
1372
|
+
))
|
|
1329
1373
|
else:
|
|
1330
1374
|
result.edges.append(_edge(
|
|
1331
1375
|
file_id, _make_id(module) if module else _make_id("package"),
|
|
@@ -1838,6 +1882,49 @@ def extract_all(entries: list[FileEntry], cfg: Config, cache: Cache | None = Non
|
|
|
1838
1882
|
# edge is left untouched (pre-fix behavior).
|
|
1839
1883
|
_MAX_METHOD_DISPATCH_CANDIDATES = 3
|
|
1840
1884
|
|
|
1885
|
+
# Languages where the reachability gate has been MEASURED. Deliberately just
|
|
1886
|
+
# Python -- see `_dispatch_is_gated` for why this is not simply "every language
|
|
1887
|
+
# whose imports bind to a file node".
|
|
1888
|
+
_DISPATCH_GATED_LANGUAGES = frozenset({"python"})
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _dispatch_is_gated(source_file: str | None) -> bool:
|
|
1892
|
+
"""Whether method dispatch from this file is restricted to reachable definitions.
|
|
1893
|
+
|
|
1894
|
+
Resolved through `LANGUAGE_BY_EXT` -- the same table `collect_files` uses to pick
|
|
1895
|
+
an extractor -- so the gate cannot disagree with the walk that produced the edge.
|
|
1896
|
+
A private suffix list here would agree with that table only by coincidence: `.py`
|
|
1897
|
+
happens to be the sole extension mapping to `python` today, and a `.pyw` or `.pyi`
|
|
1898
|
+
entry added there would silently escape a hardcoded list while still being
|
|
1899
|
+
extracted as Python.
|
|
1900
|
+
|
|
1901
|
+
FAILS OPEN, and the allowlist is narrower than the mechanism can support.
|
|
1902
|
+
|
|
1903
|
+
The gate rests on a proxy: "the caller does not import the definer" standing in
|
|
1904
|
+
for "the caller cannot be holding one of those". That proxy is never exact --
|
|
1905
|
+
every language here is duck-typed or structurally typed, so a function can be
|
|
1906
|
+
handed an instance of a class it never mentions. It is admitted for Python
|
|
1907
|
+
because it was MEASURED there (#54): on this repo it removed 1400+ edges whose
|
|
1908
|
+
every sampled member was a false binding, and added back ~350 real ones that the
|
|
1909
|
+
ambiguity cap had been abandoning.
|
|
1910
|
+
|
|
1911
|
+
TypeScript/JavaScript are NOT gated, on evidence rather than caution.
|
|
1912
|
+
`test_member_call_ambiguous_small_set_links_to_all` pins the documented
|
|
1913
|
+
small-ambiguous-set fan-out using `x: any` and no import at all -- a shape that
|
|
1914
|
+
is idiomatic in TS and that this gate would delete. Rust is ungated for the
|
|
1915
|
+
duller reason that this repo contains none, so nothing here could measure it.
|
|
1916
|
+
Extending the allowlist is a per-language measurement, not a one-line edit.
|
|
1917
|
+
|
|
1918
|
+
Go could not be gated even with evidence: its in-repo imports target a
|
|
1919
|
+
synthesized PACKAGE id (`example_com_repo_store`) that never equals the file
|
|
1920
|
+
node id of any file in that package, and files within one package see each
|
|
1921
|
+
other with no import statement at all.
|
|
1922
|
+
"""
|
|
1923
|
+
if not source_file:
|
|
1924
|
+
return False
|
|
1925
|
+
language = LANGUAGE_BY_EXT.get(Path(source_file).suffix.casefold())
|
|
1926
|
+
return language in _DISPATCH_GATED_LANGUAGES
|
|
1927
|
+
|
|
1841
1928
|
|
|
1842
1929
|
def _resolve_method_dispatch(
|
|
1843
1930
|
nodes: list[dict[str, Any]], edges: list[dict[str, Any]]
|
|
@@ -1870,6 +1957,26 @@ def _resolve_method_dispatch(
|
|
|
1870
1957
|
methods_by_name.setdefault(n["name"].casefold(), set()).add(n["id"])
|
|
1871
1958
|
known_ids = {n["id"] for n in nodes}
|
|
1872
1959
|
|
|
1960
|
+
# Which file each definition lives in, and which files each file imports
|
|
1961
|
+
# DIRECTLY. Both are needed to answer "could this caller have reached that
|
|
1962
|
+
# definition at all" -- see the reachability filter below (#54).
|
|
1963
|
+
file_of_node: dict[str, str] = {
|
|
1964
|
+
n["id"]: _file_node_id(n["source_file"]) for n in nodes if n.get("source_file")
|
|
1965
|
+
}
|
|
1966
|
+
imports_by_file: dict[str, set[str]] = {}
|
|
1967
|
+
for e in edges:
|
|
1968
|
+
if e.get("relation") == "imports" and e.get("source") and e.get("target"):
|
|
1969
|
+
imports_by_file.setdefault(e["source"], set()).add(e["target"])
|
|
1970
|
+
|
|
1971
|
+
# This gate reads import edges literally, which is only sound while they are
|
|
1972
|
+
# complete. It once carried its own ancestor-package expansion, because
|
|
1973
|
+
# `import pkg.sub` emitted no edge to `pkg/__init__.py` and the gate would
|
|
1974
|
+
# otherwise refuse `pkg.build()` -- a call on the very name that statement
|
|
1975
|
+
# binds. #55 emits that edge, so the compensation became unfalsifiable (removing
|
|
1976
|
+
# it changed no test) and was deleted rather than left looking like protection.
|
|
1977
|
+
# `test_a_dotted_import_reaches_the_package_it_binds` is what now guards the
|
|
1978
|
+
# coupling: it fails if those edges ever stop being emitted.
|
|
1979
|
+
|
|
1873
1980
|
out: list[dict[str, Any]] = []
|
|
1874
1981
|
for e in edges:
|
|
1875
1982
|
method = e.pop("_member", None) # strip from every edge, resolved or not
|
|
@@ -1877,6 +1984,23 @@ def _resolve_method_dispatch(
|
|
|
1877
1984
|
out.append(e)
|
|
1878
1985
|
continue
|
|
1879
1986
|
candidates = methods_by_name.get(method.casefold())
|
|
1987
|
+
# Reachability gate (#54). A name-only match let `Path(...).resolve()` bind
|
|
1988
|
+
# to a test double's `resolve`, and `f.write(text)` to a `write` defined in
|
|
1989
|
+
# a test file -- 232 and 30 false callers respectively, graded
|
|
1990
|
+
# decision_grade, with the mis-bindings counted as `bound` so the health
|
|
1991
|
+
# ratio rose as the defect got worse. A definition the caller neither
|
|
1992
|
+
# declares nor imports cannot be the receiver's method, whatever its name.
|
|
1993
|
+
# Filtering BEFORE the cap is deliberate: reachability is exactly the
|
|
1994
|
+
# disambiguation the cap was standing in for, so a common name with one
|
|
1995
|
+
# reachable definition now resolves instead of being abandoned as
|
|
1996
|
+
# "too generic to guess".
|
|
1997
|
+
if candidates and _dispatch_is_gated(e.get("source_file")):
|
|
1998
|
+
caller_file = _file_node_id(e["source_file"])
|
|
1999
|
+
reachable = imports_by_file.get(caller_file, frozenset())
|
|
2000
|
+
candidates = {
|
|
2001
|
+
c for c in candidates
|
|
2002
|
+
if file_of_node.get(c) == caller_file or file_of_node.get(c) in reachable
|
|
2003
|
+
}
|
|
1880
2004
|
if not candidates or len(candidates) > _MAX_METHOD_DISPATCH_CANDIDATES:
|
|
1881
2005
|
# Member call that resolves to no known definition. Keep it when
|
|
1882
2006
|
# the target is a real node, or when the call was already
|
|
@@ -266,7 +266,11 @@ class LifecycleStore:
|
|
|
266
266
|
isolation_level=None,
|
|
267
267
|
)
|
|
268
268
|
connection.row_factory = sqlite3.Row
|
|
269
|
-
|
|
269
|
+
# PRAGMA accepts no bind parameters, so this cannot be parameterized.
|
|
270
|
+
# busy_timeout_ms is an int validated to 100..30_000 at construction.
|
|
271
|
+
# Bare marker, not `nosemgrep: <id>`: semgrep namespaces rule ids by
|
|
272
|
+
# CONFIG PATH, so the real id embeds this machine's install location.
|
|
273
|
+
connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}") # nosemgrep
|
|
270
274
|
connection.execute("PRAGMA foreign_keys = ON")
|
|
271
275
|
connection.execute("PRAGMA journal_mode = WAL")
|
|
272
276
|
return connection
|
|
@@ -342,7 +346,11 @@ class LifecycleStore:
|
|
|
342
346
|
isolation_level=None,
|
|
343
347
|
)
|
|
344
348
|
connection.row_factory = sqlite3.Row
|
|
345
|
-
|
|
349
|
+
# PRAGMA accepts no bind parameters, so this cannot be parameterized.
|
|
350
|
+
# busy_timeout_ms is an int validated to 100..30_000 at construction.
|
|
351
|
+
# Bare marker, not `nosemgrep: <id>`: semgrep namespaces rule ids by
|
|
352
|
+
# CONFIG PATH, so the real id embeds this machine's install location.
|
|
353
|
+
connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}") # nosemgrep
|
|
346
354
|
connection.execute("PRAGMA foreign_keys = ON")
|
|
347
355
|
connection.execute("PRAGMA query_only = ON")
|
|
348
356
|
version = connection.execute(
|
|
@@ -440,6 +448,10 @@ class LifecycleStore:
|
|
|
440
448
|
),
|
|
441
449
|
)
|
|
442
450
|
try:
|
|
451
|
+
# The `# nosemgrep` markers below cover table IDENTIFIER interpolation.
|
|
452
|
+
# SQL binds values, never identifiers, so a migration that renames and
|
|
453
|
+
# rebuilds tables cannot use parameters. `table` is not caller input:
|
|
454
|
+
# `rebuilds` is the literal tuple defined immediately above.
|
|
443
455
|
for table, create_sql, columns in rebuilds:
|
|
444
456
|
rebuilt = create_sql.replace(
|
|
445
457
|
f"CREATE TABLE IF NOT EXISTS {table} ",
|
|
@@ -449,17 +461,17 @@ class LifecycleStore:
|
|
|
449
461
|
if rebuilt == create_sql:
|
|
450
462
|
raise LifecycleStorageError("lifecycle_migration_failed")
|
|
451
463
|
connection.execute(rebuilt)
|
|
452
|
-
connection.execute(
|
|
464
|
+
connection.execute( # nosemgrep
|
|
453
465
|
f"INSERT INTO {table}__v2 ({columns}) SELECT {columns} FROM {table}"
|
|
454
466
|
)
|
|
455
|
-
before = connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
|
456
|
-
after = connection.execute(
|
|
467
|
+
before = connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] # nosemgrep
|
|
468
|
+
after = connection.execute( # nosemgrep
|
|
457
469
|
f"SELECT COUNT(*) FROM {table}__v2"
|
|
458
470
|
).fetchone()[0]
|
|
459
471
|
if before != after:
|
|
460
472
|
raise LifecycleStorageError("lifecycle_migration_quarantined")
|
|
461
|
-
connection.execute(f"DROP TABLE {table}")
|
|
462
|
-
connection.execute(f"ALTER TABLE {table}__v2 RENAME TO {table}")
|
|
473
|
+
connection.execute(f"DROP TABLE {table}") # nosemgrep
|
|
474
|
+
connection.execute(f"ALTER TABLE {table}__v2 RENAME TO {table}") # nosemgrep
|
|
463
475
|
if connection.execute("PRAGMA foreign_key_check").fetchall():
|
|
464
476
|
raise LifecycleStorageError("lifecycle_migration_quarantined")
|
|
465
477
|
integrity = connection.execute("PRAGMA integrity_check").fetchone()
|
|
@@ -501,9 +513,11 @@ class LifecycleStore:
|
|
|
501
513
|
}
|
|
502
514
|
if not set(expected) <= actual:
|
|
503
515
|
raise LifecycleStorageError("lifecycle_rollback_required")
|
|
516
|
+
# Identifier interpolation again, and `table` comes from the `expected`
|
|
517
|
+
# literal above -- never from a caller. PRAGMA takes no bind parameters.
|
|
504
518
|
for table, required_columns in expected.items():
|
|
505
519
|
columns = {
|
|
506
|
-
str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})")
|
|
520
|
+
str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})") # nosemgrep
|
|
507
521
|
}
|
|
508
522
|
if not required_columns <= columns:
|
|
509
523
|
raise LifecycleStorageError("lifecycle_rollback_required")
|
|
@@ -902,7 +902,11 @@ class RepositoryStore:
|
|
|
902
902
|
isolation_level=None,
|
|
903
903
|
)
|
|
904
904
|
connection.row_factory = sqlite3.Row
|
|
905
|
-
|
|
905
|
+
# PRAGMA accepts no bind parameters, so this cannot be parameterized.
|
|
906
|
+
# busy_timeout_ms is an int validated to 100..30_000 at construction.
|
|
907
|
+
# Bare marker, not `nosemgrep: <id>`: semgrep namespaces rule ids by
|
|
908
|
+
# CONFIG PATH, so the real id embeds this machine's install location.
|
|
909
|
+
connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}") # nosemgrep
|
|
906
910
|
connection.execute("PRAGMA foreign_keys = ON")
|
|
907
911
|
connection.execute("PRAGMA journal_mode = WAL")
|
|
908
912
|
return connection
|
|
@@ -1245,21 +1249,26 @@ class RepositoryStore:
|
|
|
1245
1249
|
)
|
|
1246
1250
|
connection.execute("PRAGMA foreign_keys=OFF")
|
|
1247
1251
|
connection.execute("BEGIN IMMEDIATE")
|
|
1252
|
+
# The `# nosemgrep` markers below cover table IDENTIFIER interpolation.
|
|
1253
|
+
# SQL binds values, never identifiers, so a migration that copies and
|
|
1254
|
+
# rebuilds tables cannot use parameters. `table` is not caller input:
|
|
1255
|
+
# `_PRE_V6_WIDENED_TABLES` is a module-level `Final` mapping of two
|
|
1256
|
+
# literal table names.
|
|
1248
1257
|
for table, (ddl, columns) in _PRE_V6_WIDENED_TABLES.items():
|
|
1249
|
-
before = connection.execute(
|
|
1258
|
+
before = connection.execute( # nosemgrep
|
|
1250
1259
|
f"SELECT COUNT(*) FROM {table}"
|
|
1251
1260
|
).fetchone()[0]
|
|
1252
|
-
connection.execute(
|
|
1261
|
+
connection.execute( # nosemgrep
|
|
1253
1262
|
f"CREATE TABLE {table}_pre_v6_copy AS SELECT * FROM {table}"
|
|
1254
1263
|
)
|
|
1255
|
-
connection.execute(f"DROP TABLE {table}")
|
|
1264
|
+
connection.execute(f"DROP TABLE {table}") # nosemgrep
|
|
1256
1265
|
connection.execute(ddl)
|
|
1257
1266
|
connection.execute(
|
|
1258
1267
|
f"INSERT INTO {table} ({columns}) "
|
|
1259
1268
|
f"SELECT {columns} FROM {table}_pre_v6_copy"
|
|
1260
1269
|
)
|
|
1261
|
-
connection.execute(f"DROP TABLE {table}_pre_v6_copy")
|
|
1262
|
-
after = connection.execute(
|
|
1270
|
+
connection.execute(f"DROP TABLE {table}_pre_v6_copy") # nosemgrep
|
|
1271
|
+
after = connection.execute( # nosemgrep
|
|
1263
1272
|
f"SELECT COUNT(*) FROM {table}"
|
|
1264
1273
|
).fetchone()[0]
|
|
1265
1274
|
if before != after:
|
|
@@ -1318,21 +1327,26 @@ class RepositoryStore:
|
|
|
1318
1327
|
)
|
|
1319
1328
|
connection.execute("PRAGMA foreign_keys=OFF")
|
|
1320
1329
|
connection.execute("BEGIN IMMEDIATE")
|
|
1330
|
+
# The `# nosemgrep` markers below cover table IDENTIFIER interpolation.
|
|
1331
|
+
# SQL binds values, never identifiers, so a migration that copies and
|
|
1332
|
+
# rebuilds tables cannot use parameters. `table` is not caller input:
|
|
1333
|
+
# `_PRE_V6_WIDENED_TABLES` is a module-level `Final` mapping of two
|
|
1334
|
+
# literal table names.
|
|
1321
1335
|
for table, (ddl, columns) in _PRE_V6_WIDENED_TABLES.items():
|
|
1322
|
-
before = connection.execute(
|
|
1336
|
+
before = connection.execute( # nosemgrep
|
|
1323
1337
|
f"SELECT COUNT(*) FROM {table}"
|
|
1324
1338
|
).fetchone()[0]
|
|
1325
|
-
connection.execute(
|
|
1339
|
+
connection.execute( # nosemgrep
|
|
1326
1340
|
f"CREATE TABLE {table}_pre_v7_copy AS SELECT * FROM {table}"
|
|
1327
1341
|
)
|
|
1328
|
-
connection.execute(f"DROP TABLE {table}")
|
|
1342
|
+
connection.execute(f"DROP TABLE {table}") # nosemgrep
|
|
1329
1343
|
connection.execute(ddl)
|
|
1330
1344
|
connection.execute(
|
|
1331
1345
|
f"INSERT INTO {table} ({columns}) "
|
|
1332
1346
|
f"SELECT {columns} FROM {table}_pre_v7_copy"
|
|
1333
1347
|
)
|
|
1334
|
-
connection.execute(f"DROP TABLE {table}_pre_v7_copy")
|
|
1335
|
-
after = connection.execute(
|
|
1348
|
+
connection.execute(f"DROP TABLE {table}_pre_v7_copy") # nosemgrep
|
|
1349
|
+
after = connection.execute( # nosemgrep
|
|
1336
1350
|
f"SELECT COUNT(*) FROM {table}"
|
|
1337
1351
|
).fetchone()[0]
|
|
1338
1352
|
if before != after:
|
|
@@ -3263,10 +3277,14 @@ class RepositoryStore:
|
|
|
3263
3277
|
return tuple(dict(row) for row in rows)
|
|
3264
3278
|
|
|
3265
3279
|
def row_count(self, table: str) -> int:
|
|
3280
|
+
# The one site here that DOES take a caller-supplied name, which is why the
|
|
3281
|
+
# allowlist below is load-bearing rather than decorative: `table` is
|
|
3282
|
+
# rejected unless it is one of the known table names, and only then reaches
|
|
3283
|
+
# the query. A table identifier cannot be passed as a bind parameter.
|
|
3266
3284
|
if table not in _TABLES:
|
|
3267
3285
|
raise ValueError("table_invalid")
|
|
3268
3286
|
with self._connection() as connection:
|
|
3269
|
-
return int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])
|
|
3287
|
+
return int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]) # nosemgrep
|
|
3270
3288
|
|
|
3271
3289
|
def save_capability_snapshot_record(self, snapshot: CapabilitySnapshot) -> bool:
|
|
3272
3290
|
if not isinstance(snapshot, CapabilitySnapshot):
|