theurian 0.1.0.dev3__tar.gz → 0.1.0.dev4__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.
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/CHANGELOG.md +274 -1
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/PKG-INFO +1 -1
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/pyproject.toml +1 -1
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/__init__.py +1 -1
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/index_builder.py +32 -1
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/project_service.py +177 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/retrieval_service.py +5 -2
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/withdrawal_purge.py +11 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/cli/commands.py +136 -15
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/cli/index_commands.py +25 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/knowledge.py +29 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/canonical_store.py +35 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/schema.py +92 -12
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/store.py +142 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/mcp/search.py +72 -9
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/mcp/tools.py +181 -76
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_absence_proof.py +55 -4
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_canonical_store.py +77 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_canonical_store_corruption.py +467 -166
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_mcp_tools.py +763 -16
- theurian-0.1.0.dev4/tests/integration/test_state_provenance.py +856 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/mcp/knowledge-search-response.schema.json +2 -2
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/mcp/knowledge-status-response.schema.json +2 -2
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/mcp/retrieval-metadata.schema.json +4 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/.gitignore +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/LICENSE +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/README.md +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/hatch_build.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/forest_builder.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/ingestion_service.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/migration_engine.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/setup_context.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/setup_service.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/setup_steps.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/setup_withholding.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/application/visibility.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/cli/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/cli/auth_commands.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/cli/context.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/cli/main.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/cli/setup_commands.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/daemon/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/daemon/instance.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/daemon/runner.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/daemon/server.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/chunking.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/compatibility.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/context.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/enums.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/errors.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/extras.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/identifiers.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ingestion.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/migration.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/authorization.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/daemon_manager.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/determinism.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/embedding.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/index_store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/mcp_client_config.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/object_store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/reranking.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/review_provider.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/secret_store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/source_parser.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/specification_provider.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/summarization.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ports/vector_store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/project.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/ranking.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/raptor.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/retrieval.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/review.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/setup.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/specification.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/state.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/domain/values.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/indexing/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/claude/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/claude/mcp_config.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/determinism.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/embedding/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/embedding/hashing.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/filesystem/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/filesystem/migration_loader.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/filesystem/parsers/markdown.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/filesystem/parsers/openapi.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/filesystem/parsers/registry.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/filesystem/parsers/structured.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/git/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/github/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/raptor/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/raptor/extractive.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/secrets/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/secrets/file_store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/services/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/services/launchagent.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/services/runner.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/services/systemd_user.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/connection.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/index_forest.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/index_purge.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/index_query.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/index_scan.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/index_schema.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/sqlite/index_store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/infrastructure/vector/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/ingestion/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/mcp/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/mcp/results.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/migrations/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/normalization/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/normalization/projection.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/observability/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/retrieval/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/review/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/security/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/security/env_file.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/security/paths.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/security/tokens.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/security/yaml_loading.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/specification/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/src/theurian/traceability/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/conftest.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/fakes/__init__.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/fakes/clock.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/fakes/ids.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/fakes/pages.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/fakes/setup.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/fakes/store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_auth_rotate.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_bare_install.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_claude_mcp_config.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_cli_commands.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_cli_help_without_rich.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_daemon.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_forest_builder.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_forest_builder_scale.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_forest_node_scope.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_forest_purge_equality.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_forest_purge_recompute.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_forest_retrieval.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_forest_store_retrieval.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_gc_during_a_search.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_fallback.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_gc_cli.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_purge.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_purge_differential.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_purge_nodes.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_scan_fold.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_schema_v4.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_index_store.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_ingestion.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_init_gitignore_block.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_project_registry.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_retrieval_service.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_revision_id_reuse.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_scan_exhaustion.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_service_adapters.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_session_start_hook.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_setup_cli.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_setup_env_file.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_setup_journal.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_setup_partial_failure.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_setup_probe_assertions.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_setup_report_withholding.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_setup_service.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_short_query_retrieval.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_unreadable_registry_surface.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_wire_contract.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_withdrawal_purge.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/integration/test_writer_contract.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_artifact_integrity_claim.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_candidate_cut.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_candidate_depth.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_chunking.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_cli_help_rendering.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_compatibility.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_daemon_extra.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_domain_invariants.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_env_file_merge.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_examples.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_extractive_summarizer.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_forest_derivation.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_forest_fanout.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_gate_call_sites.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_hashing_embedding.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_install_claims.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_layering.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_migration_engine.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_parsers.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_path_security.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_plugin_boundary.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_ports.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_project_and_traceability.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_projection.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_ranking.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_raptor_scope.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_result_gate_session.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_result_payload.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_retrieval_depth.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_schemas.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_scope_isolation.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_setup_changed_paths.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_setup_claims.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_setup_domain.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_state_hash.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_test_fixtures.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_tokens.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/tests/unit/test_yaml_loading.py +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/README.md +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/cli/version.schema.json +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/config/project-config.schema.json +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/knowledge/retrieval-result.schema.json +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/mcp/project-list-response.schema.json +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/mcp/tool-context.schema.json +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/migrations/migration.schema.json +0 -0
- {theurian-0.1.0.dev3 → theurian-0.1.0.dev4}/theurian/schemas/protocol/compatibility.schema.json +0 -0
|
@@ -12,6 +12,275 @@ Pre-1.0, a MINOR bump may change the protocol. Post-1.0, only a MAJOR may.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.1.0.dev4] - 2026-08-16
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **The `integrity` signal takes a second measurement: how many items a caller
|
|
20
|
+
should be able to see, against the number `theurian migrate apply` recorded**
|
|
21
|
+
([#30](https://github.com/theurian/theurian/issues/30) PR2). This is what makes
|
|
22
|
+
a response's *own* emptiness visible, where PR1 — shipped in `0.1.0.dev3` —
|
|
23
|
+
could only report damage elsewhere in the state. Three of the four positions
|
|
24
|
+
PR1 left now disclose, and `SILENTLY_EMPTIED` is deleted — #30's stated closure
|
|
25
|
+
condition.
|
|
26
|
+
|
|
27
|
+
The record is a new `project_integrity` table, one row per project, written by
|
|
28
|
+
`migrate apply` inside its own write transaction and counted over the rows that
|
|
29
|
+
transaction just wrote. Nothing on a query path computes it: the reader's half
|
|
30
|
+
is one `COUNT` over `idx_items_status`, catching a change in the *number* of
|
|
31
|
+
surfaceable rows — a row leaving or entering the surfaceable scope, in either
|
|
32
|
+
direction — so an expectation cannot be satisfied by the state it is meant to
|
|
33
|
+
check
|
|
34
|
+
(`CanonicalStore.count_surfaceable_items`,
|
|
35
|
+
`CanonicalStore.expected_surfaceable_count`, `SqliteWriter.record_expected_surfaceable_count`).
|
|
36
|
+
|
|
37
|
+
**A missing record is damage, not "not recorded", and a schema bump is what
|
|
38
|
+
makes that sound.** `SCHEMA_VERSION` went 2 → 3 for this table (below), and
|
|
39
|
+
`is_supported` is exact-match, so every database this build can open was written
|
|
40
|
+
by a build that records. `test_a_missing_integrity_record_is_damage_and_not_silence`
|
|
41
|
+
holds the inference and `test_a_pre_integrity_database_is_refused_unread_by_every_tool`
|
|
42
|
+
holds the premise on all three tools and over every version below the current
|
|
43
|
+
one — including that an old database is not reported as a damaged one.
|
|
44
|
+
|
|
45
|
+
**What it now catches, pinned by the corruption sweep against the real tools
|
|
46
|
+
over a real damaged database.** A sentinel in `knowledge_items.project_id`
|
|
47
|
+
takes every item out of the project scope, and one in `knowledge_items.status`
|
|
48
|
+
takes a row out of the
|
|
49
|
+
surfaceable scope: `knowledge.search` answers `count: 0, results: []` and
|
|
50
|
+
`knowledge.status` answers a shrunken `itemCount` **with the key beside them**,
|
|
51
|
+
where PR1 answered the same numbers alone; `knowledge.get` refuses those cells
|
|
52
|
+
as damage rather than reporting them as absence
|
|
53
|
+
(`test_a_lost_surfaceable_item_is_damage_on_every_read_tool`,
|
|
54
|
+
`test_a_lost_surfaceable_item_makes_get_refuse_an_absent_id_as_damage`). So an
|
|
55
|
+
agent can now tell "this project holds nothing" from "part of this project could
|
|
56
|
+
not be read", and `knowledge.status` no longer publishes a positive
|
|
57
|
+
`appliedMigrations` beside `itemCount: 0` without comment. The `status` cell is
|
|
58
|
+
the member `0.1.0.dev2` recorded as "the fifth `SILENTLY_EMPTIED` member,
|
|
59
|
+
carried to Milestone 6" when #19 stopped parsing every row: it still
|
|
60
|
+
under-reports, because no read tool can repair a row it cannot parse, but it no
|
|
61
|
+
longer does so in silence.
|
|
62
|
+
|
|
63
|
+
**`knowledge.get`'s damage refusal is reworded, and deliberately says less.**
|
|
64
|
+
`0.1.0.dev3` reported a project that "could not be fully read: its derived state
|
|
65
|
+
holds a different number of migration-history rows than its own records expect".
|
|
66
|
+
Either comparison now reaches that branch, so the text says only that the
|
|
67
|
+
derived state "disagrees with its own records about what it holds" and names no
|
|
68
|
+
record — a caller matching on the old substring stops matching. The SEC-13 rule
|
|
69
|
+
that a withheld id and an absent id get the same message as each other is
|
|
70
|
+
unchanged, and so is the pair that pins both directions
|
|
71
|
+
(`test_an_absent_item_over_a_damaged_state_is_refused_as_damage_not_absence`,
|
|
72
|
+
`test_an_absent_item_over_a_healthy_state_is_refused_as_absence`).
|
|
73
|
+
|
|
74
|
+
**One position stays silent and is named rather than rounded away.**
|
|
75
|
+
`(knowledge.search, knowledge_items, item_id)` moves neither count — the row
|
|
76
|
+
keeps its `project_id` and its `status`, so both sides still count it — while
|
|
77
|
+
the item → revision pointer the search walks is broken, and the tool answers one
|
|
78
|
+
result short with no key. A count is not a checksum, and that is the shape a
|
|
79
|
+
count cannot see. It is `UNDETECTED_UNDERREPORT` in
|
|
80
|
+
`tests/integration/test_canonical_store_corruption.py`, an exact set of exactly
|
|
81
|
+
one member: a second position appearing there is a failure, not an expectation
|
|
82
|
+
to update.
|
|
83
|
+
|
|
84
|
+
**`SILENTLY_EMPTIED` is replaced by three sets that partition the same
|
|
85
|
+
question**, so no position can slide between outcomes unremarked.
|
|
86
|
+
`DISCLOSED_AS_INTEGRITY` holds the nine positions that fire the key, keyed on
|
|
87
|
+
the key's presence and nothing else — six of them fire while every integer in
|
|
88
|
+
the response stays put, which is the detector's true shape;
|
|
89
|
+
`DISCLOSED_BESIDE_A_SHRUNKEN_COUNT` holds the three that also shrink a published
|
|
90
|
+
integer; `UNDETECTED_UNDERREPORT` holds the one that shrinks and says nothing.
|
|
91
|
+
The sweep asserts the whole shrinking class equals the union of the last two
|
|
92
|
+
(`test_exactly_these_positions_disclose_damage_as_integrity`,
|
|
93
|
+
`test_exactly_one_position_answers_with_less_than_the_file_holds_and_says_nothing`
|
|
94
|
+
— renamed and re-scoped from PR1's
|
|
95
|
+
`test_exactly_these_positions_disclose_migration_history_damage_as_integrity`
|
|
96
|
+
and `test_no_tool_answers_with_less_than_the_intact_database_holds`), which is
|
|
97
|
+
what closes the seam a pair of independently-keyed sets would leave. That
|
|
98
|
+
partition is over the swept single-cell positions, not a claim that the count
|
|
99
|
+
catches every wrong answer: a status moved *within* `SURFACEABLE_STATUSES`
|
|
100
|
+
changes the set's composition without its size and sets no key, and an item
|
|
101
|
+
whose `current_revision_id` names another item's revision would disclose rather
|
|
102
|
+
than under-report and is refused at read time by the read-back guard
|
|
103
|
+
(`61747b3`), a mechanism distinct from this detector.
|
|
104
|
+
|
|
105
|
+
**Neither side of the new comparison counts a row the caller may not read.**
|
|
106
|
+
Both count `SURFACEABLE_STATUSES` — at build time in the `INSERT … SELECT`, at
|
|
107
|
+
read time in the `COUNT` — so a `rejected`, `deprecated` or `superseded` row is
|
|
108
|
+
on neither side.
|
|
109
|
+
`test_the_integrity_signal_is_identical_across_a_withheld_only_difference` holds
|
|
110
|
+
that whether the key appears is identical across two corpora differing only in
|
|
111
|
+
twenty-five `rejected` items. Measured beyond what that pins: overwriting a
|
|
112
|
+
`deprecated` row's `status` produces no key on any tool, where the same
|
|
113
|
+
overwrite on an `approved` row fires it, and on a `draft` row it fires the key
|
|
114
|
+
while the default `knowledge.search` answer stays unchanged — a draft is
|
|
115
|
+
surfaceable even when the default answer omits it. The mirror of that is a
|
|
116
|
+
recorded residual: overwriting an `approved` row's `status` to another
|
|
117
|
+
surfaceable value moves neither count and sets no key, while the default answer
|
|
118
|
+
loses a row it used to surface — the count measures the size of the surfaceable
|
|
119
|
+
set, not its composition, and the moved row is caller-readable either way. The
|
|
120
|
+
read cost keeps PR1's
|
|
121
|
+
shape, `O(surfaceable)` over the covering index rather than `O(total)`, and
|
|
122
|
+
`knowledge.status` spends no extra query at all: it sums the breakdown it had
|
|
123
|
+
already read.
|
|
124
|
+
|
|
125
|
+
**The remedy's first step deliberately does not clear this shape, and the
|
|
126
|
+
residual is recorded.** `migrate apply` records only when it created the
|
|
127
|
+
database or applied a migration; an apply with nothing pending must not
|
|
128
|
+
re-record, because it is step one of the remedy this signal publishes and
|
|
129
|
+
re-recording from the damaged state would manufacture its own all-clear
|
|
130
|
+
(`test_a_pending_free_apply_does_not_re_record_over_a_damaged_state`,
|
|
131
|
+
`test_an_apply_that_changes_the_store_records_the_new_count`). What stays open,
|
|
132
|
+
recorded and not fixed: an apply that *does* have a migration to apply re-records
|
|
133
|
+
over the state as it then is, so damage already present becomes the new
|
|
134
|
+
expectation. It is the pointer's limit again — a count is not a checksum, and a
|
|
135
|
+
writer can record only what it can read.
|
|
136
|
+
|
|
137
|
+
The detector also found a defect in the test suite it does not own.
|
|
138
|
+
`tests/integration/test_absence_proof.py` built its pair by hand with a pointer
|
|
139
|
+
claiming one applied migration over a store holding none, so every response in
|
|
140
|
+
that file had carried `integrity` since PR1 and each equality was comparing two
|
|
141
|
+
damage reports. Both records are now written the way `migrate apply` writes
|
|
142
|
+
them, and `_assert_the_pair_bites` fails an example that answers as a damaged
|
|
143
|
+
project — the fifth way that file can be green while proving nothing.
|
|
144
|
+
|
|
145
|
+
### Changed
|
|
146
|
+
|
|
147
|
+
- **BREAKING — `SCHEMA_VERSION` 2 → 3: every existing state database is refused
|
|
148
|
+
once, and one `theurian migrate apply` rebuilds it**
|
|
149
|
+
([#30](https://github.com/theurian/theurian/issues/30) PR2,
|
|
150
|
+
[#24](https://github.com/theurian/theurian/issues/24)). The DDL gains the
|
|
151
|
+
`project_integrity` table and makes the item → revision pointer a composite
|
|
152
|
+
foreign key; `knowledge.status` therefore publishes `schemaVersion: 3` where it
|
|
153
|
+
published `2`. A state database is derived and Git-ignored (ADR-0004) and is
|
|
154
|
+
rebuilt rather than migrated (ADR-0017), so nothing authored is lost — but the
|
|
155
|
+
rebuild is not automatic, and until it runs the three read tools refuse.
|
|
156
|
+
BREAKING for a state database on disk, not for the wire: a published value
|
|
157
|
+
moves and no field, type or tool name does, so `protocolVersion` stays
|
|
158
|
+
`theurian/v1` (see *Changing this contract* in
|
|
159
|
+
[`docs/protocol/mcp-tools.md`](../../docs/protocol/mcp-tools.md)).
|
|
160
|
+
|
|
161
|
+
Measured end to end rather than argued, against a database `0.1.0.dev3` wrote
|
|
162
|
+
with the real CLI and then read by this build: `knowledge.search`,
|
|
163
|
+
`knowledge.get` and `knowledge.status` each refuse with
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
theurian-state-f1711b98d302.sqlite was written at schema version 2, but this
|
|
167
|
+
build uses 3. State databases are derived; rebuild with `theurian migrate
|
|
168
|
+
apply` rather than migrating this file.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
and one `theurian migrate apply` answers `databaseCreated: true` with a new
|
|
172
|
+
state hash — the schema version is an input to that hash, so the rebuilt file is
|
|
173
|
+
`theurian-state-2e8880bf25be.sqlite` beside the old one — after which all three
|
|
174
|
+
tools answer, `schemaVersion` reads `3`, and no `integrity` key appears. The old
|
|
175
|
+
file is left on disk for `theurian index gc` (ADR-0017 decision 5), not deleted
|
|
176
|
+
under a pinned `snapshotId`.
|
|
177
|
+
|
|
178
|
+
A database written at *any* earlier version is refused *unread* rather than
|
|
179
|
+
reinterpreted, which is what lets the new detector read a missing
|
|
180
|
+
`project_integrity` row as damage rather than as "this file predates the table"
|
|
181
|
+
(`test_a_pre_integrity_database_is_refused_unread_by_every_tool`, parametrised
|
|
182
|
+
over every version below the current one).
|
|
183
|
+
|
|
184
|
+
- **The item → revision pointer is scoped to its project by the schema, not only
|
|
185
|
+
by every read of it** ([#24](https://github.com/theurian/theurian/issues/24),
|
|
186
|
+
closed). `knowledge_items.current_revision_id` referenced
|
|
187
|
+
`knowledge_revisions(revision_id)` alone while `get_revision` and
|
|
188
|
+
`list_revisions` both filter on `project_id` as well, so the two never met: a
|
|
189
|
+
revision whose `project_id` moved — what a project id changing over an unchanged
|
|
190
|
+
root does — left the item pointing at a row its own project-scoped read could no
|
|
191
|
+
longer see, and `PRAGMA foreign_key_check` called the database satisfied.
|
|
192
|
+
|
|
193
|
+
The key is now composite, `(project_id, current_revision_id)` referencing
|
|
194
|
+
`knowledge_revisions(project_id, revision_id)` over a new unique index. Measured
|
|
195
|
+
on SQLite 3.51.2, before and after, against the stranding `UPDATE`: before, the
|
|
196
|
+
writer's own connection accepted it and `foreign_key_check` returned `[]`;
|
|
197
|
+
after, the same statement is refused, and forced through with foreign keys off
|
|
198
|
+
it is reported as `('knowledge_items', <rowid>, 'knowledge_revisions', 0)`.
|
|
199
|
+
`test_a_revision_cannot_be_moved_out_from_under_the_item_pointing_at_it` holds
|
|
200
|
+
both arms — the stranding move refused, a revision no item points at still
|
|
201
|
+
movable — so a key that refused every write to that column would fail it too.
|
|
202
|
+
|
|
203
|
+
A `NULL` `current_revision_id` still satisfies the key, because a composite
|
|
204
|
+
child key with a NULL component imposes no constraint in SQLite; an item exists
|
|
205
|
+
before its first revision is upserted. The four `# pragma: no cover` branches
|
|
206
|
+
whose justification was "the pointer is a foreign key" now say what actually
|
|
207
|
+
holds them, and the claim is true for the first time. INV-2's other half — that
|
|
208
|
+
the revision belongs to the same *item* — is enforced above the database and in
|
|
209
|
+
nothing the schema declares: by `KnowledgeItem.with_revision` in the domain, and
|
|
210
|
+
by `append_revision` and `put_item` in both `MigrationWriter` adapters.
|
|
211
|
+
|
|
212
|
+
### Security
|
|
213
|
+
|
|
214
|
+
- **Derived state under `.theurian/state/` is served only if this installation
|
|
215
|
+
built it (threat-model T-19, ADR-0004, SEC-7).** Everything there — the active
|
|
216
|
+
pointers and the SQLite databases they name — is derived and git-ignored, but a
|
|
217
|
+
repository contributor can force-add a doctored copy past that ignore (`git add
|
|
218
|
+
-f`), and a victim who clones (or downloads the ZIP/tarball), `theurian project
|
|
219
|
+
register`s, and serves over MCP **without ever running `theurian migrate
|
|
220
|
+
apply`** was served the attacker's bytes: a `rejected` body relabelled
|
|
221
|
+
`approved`, rows injected, titles and excerpts rewritten in the index.
|
|
222
|
+
|
|
223
|
+
This is the self-consistent face the read-back guards cannot catch. The #30 PR2
|
|
224
|
+
detector and the item → revision pointer guard above find a derived state that
|
|
225
|
+
*disagrees with its own records*; this attacker authors both sides, at the
|
|
226
|
+
current schema version, so there is no inconsistency to find. `active.json`'s
|
|
227
|
+
`stateHash` binds the migration *set*, not the database bytes, and the database
|
|
228
|
+
filename is derived from that hash, so the doctored pair is self-consistent by
|
|
229
|
+
construction. The only property a repository author cannot forge is whether
|
|
230
|
+
*this installation* built the artifact.
|
|
231
|
+
|
|
232
|
+
**Control: an out-of-tree build-provenance anchor.** `theurian migrate apply`
|
|
233
|
+
and `theurian index build` record the state hash and index build id this
|
|
234
|
+
install produced for each project root in `THEURIAN_DATA_DIR/provenance.json` —
|
|
235
|
+
beside the project registry, out of the repository tree where a contributor
|
|
236
|
+
cannot write (`BuildProvenance`). Every MCP read path checks it before a byte of
|
|
237
|
+
`.theurian/state/` reaches a caller: `_resolve` refuses a canonical state whose
|
|
238
|
+
hash this install did not build (`verify_state_provenance`, covering
|
|
239
|
+
`knowledge.get`, `knowledge.search`, `knowledge.status`); the ranked path stands
|
|
240
|
+
aside from an index build id this install did not build. Both paths that
|
|
241
|
+
generate an index are gated on source-index provenance, so neither launders a
|
|
242
|
+
committed one: `index build` refuses to build *from* an unprovenanced canonical
|
|
243
|
+
state, and — since commit `dc6aa79` — the withdrawal purge refuses to copy a
|
|
244
|
+
committed index forward and record it when this install did not build the source
|
|
245
|
+
index (`UNTRUSTED_SOURCE_INDEX`), the second laundering path review found. And
|
|
246
|
+
`migrate apply` discards an unprovenanced database and rebuilds from the
|
|
247
|
+
Git-tracked migrations; a committed `-wal` cannot replay because
|
|
248
|
+
`create_database` refuses to write over an existing file, so the rebuild deletes
|
|
249
|
+
the main database and creates a fresh one with no database for the sidecar to
|
|
250
|
+
replay into — the sidecars are removed too, redundant defense-in-depth.
|
|
251
|
+
Those migrations are vouched for by human PR review (T-1), not by FR-K5: on a
|
|
252
|
+
fresh clone nothing has been applied, so FR-K5 has no recorded checksum to
|
|
253
|
+
disagree with an attacker-authored migration, and re-derivation is safe because
|
|
254
|
+
a reviewer read the migration diff.
|
|
255
|
+
|
|
256
|
+
Delivery-independent by construction: the discriminator is "did this install
|
|
257
|
+
build it", not "is it tracked by Git", so a clone, a ZIP download and a
|
|
258
|
+
repackaged tarball are refused alike — which a `git ls-files` probe could not
|
|
259
|
+
do. Pinned by `tests/integration/test_state_provenance.py`, whose closure
|
|
260
|
+
invariant is one query against two checkouts: a checkout shipping derived state
|
|
261
|
+
and one shipping none produce identical served knowledge, both refused until the
|
|
262
|
+
state is built locally.
|
|
263
|
+
|
|
264
|
+
**Effect on existing installs, and it is deliberate.** A project already built
|
|
265
|
+
by a pre-`0.1.0.dev4` build has no provenance record, so the three read tools
|
|
266
|
+
refuse it until one `theurian migrate apply` (then `theurian index build`)
|
|
267
|
+
rebuilds it and records provenance — the same one-command rebuild the
|
|
268
|
+
`SCHEMA_VERSION` bump above already requires, and nothing authored is lost
|
|
269
|
+
(ADR-0004). The residual is recorded in T-19: provenance vouches for a hash, not
|
|
270
|
+
for the database bytes, so replacing a database *after* this install built the
|
|
271
|
+
matching hash is out of scope for this control and left to the schema gate, the
|
|
272
|
+
#30 read-back guards, and the corruption checks.
|
|
273
|
+
|
|
274
|
+
<!-- Advisory (GHSA) reference for T-19 to be filled at release, when the
|
|
275
|
+
advisory ships; embargoed until then. -->
|
|
276
|
+
|
|
277
|
+
- **`knowledge.search` gains one `fallbackReason`, `index-unbuilt`**, emitted when
|
|
278
|
+
the published index was not built by this installation and the ranked path
|
|
279
|
+
therefore stands aside to the provenance-gated canonical scan. A new published
|
|
280
|
+
*value*, not a new field, type or tool, so `protocolVersion` stays `theurian/v1`
|
|
281
|
+
by the same rule the `SCHEMA_VERSION` entry above applies; the JSON Schema
|
|
282
|
+
`schemas/mcp/retrieval-metadata.schema.json` adds the `const`.
|
|
283
|
+
|
|
15
284
|
## [0.1.0.dev3] - 2026-08-15
|
|
16
285
|
|
|
17
286
|
### Added
|
|
@@ -179,6 +448,9 @@ Pre-1.0, a MINOR bump may change the protocol. Post-1.0, only a MAJOR may.
|
|
|
179
448
|
with is now a refusal. Only negative values are refused — a non-negative integer
|
|
180
449
|
that is simply wrong is still accepted, which is the one-way limit recorded
|
|
181
450
|
above and in the threat model.
|
|
451
|
+
|
|
452
|
+
- **`knowledge.status` no longer refuses over a corrupt
|
|
453
|
+
`migration_history.migration_id` or `checksum`; it answers cleanly**
|
|
182
454
|
([#30](https://github.com/theurian/theurian/issues/30) PR1). The refusal was a
|
|
183
455
|
side effect of parsing rows the tool no longer reads: it used to call
|
|
184
456
|
`applied_migrations`, which converts both cells and raises on a damaged one,
|
|
@@ -434,7 +706,8 @@ Pre-1.0, a MINOR bump may change the protocol. Post-1.0, only a MAJOR may.
|
|
|
434
706
|
|
|
435
707
|
- **A reused `revisionId` across two items no longer leaks a withheld item's body**
|
|
436
708
|
([GHSA-7997-g35f-q59h](https://github.com/theurian/theurian/security/advisories/GHSA-7997-g35f-q59h);
|
|
437
|
-
|
|
709
|
+
fixed in [`67c0e81`](https://github.com/theurian/theurian/commit/67c0e81)).
|
|
710
|
+
**BREAKING (state database schema).**
|
|
438
711
|
In 0.1.0.dev0–0.1.0.dev2 a migration that reused an existing `revisionId` under
|
|
439
712
|
a second `itemId` — the shape a copy-pasted `upsertRevision` block produces —
|
|
440
713
|
pointed the second (approved) item's current revision at the first item's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: theurian
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
4
|
Summary: Git-native engineering knowledge platform: versioned, traceable context for AI agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/theurian/theurian
|
|
6
6
|
Project-URL: Documentation, https://github.com/theurian/theurian/tree/main/docs
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "theurian"
|
|
7
|
-
version = "0.1.0.
|
|
7
|
+
version = "0.1.0.dev4"
|
|
8
8
|
description = "Git-native engineering knowledge platform: versioned, traceable context for AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -8,7 +8,7 @@ plugin must never import them (ADR-0001, CP-2).
|
|
|
8
8
|
|
|
9
9
|
from theurian.domain.compatibility import CURRENT_PROTOCOL_VERSION
|
|
10
10
|
|
|
11
|
-
__version__ = "0.1.0.
|
|
11
|
+
__version__ = "0.1.0.dev4"
|
|
12
12
|
__protocol_version__ = CURRENT_PROTOCOL_VERSION
|
|
13
13
|
|
|
14
14
|
__all__ = ["__protocol_version__", "__version__"]
|
|
@@ -36,6 +36,7 @@ from theurian.application.forest_builder import ForestBuilder
|
|
|
36
36
|
from theurian.domain.chunking import IndexableChunk, chunk_document
|
|
37
37
|
from theurian.domain.context import RequestContext
|
|
38
38
|
from theurian.domain.enums import may_surface
|
|
39
|
+
from theurian.domain.errors import InvariantViolationError
|
|
39
40
|
from theurian.domain.identifiers import ProjectId
|
|
40
41
|
from theurian.domain.ports.canonical_store import CanonicalReadSession
|
|
41
42
|
from theurian.domain.ports.embedding import EmbeddingProvider
|
|
@@ -147,8 +148,38 @@ class IndexBuilder:
|
|
|
147
148
|
if item.current_revision_id is None:
|
|
148
149
|
continue
|
|
149
150
|
revision = store.get_revision(context, item.current_revision_id)
|
|
150
|
-
if revision is None: # pragma: no cover -
|
|
151
|
+
if revision is None: # pragma: no cover - a composite foreign key holds this (#24)
|
|
151
152
|
continue
|
|
153
|
+
if not item.owns(revision):
|
|
154
|
+
# A `current_revision_id` naming a sibling item's revision is
|
|
155
|
+
# type-valid, satisfies the composite foreign key, and moves
|
|
156
|
+
# neither `#30` integrity count, so nothing upstream of this
|
|
157
|
+
# loop refuses it. Read through here it is the one face of
|
|
158
|
+
# that damage the query side cannot then catch:
|
|
159
|
+
# `CanonicalVisibility._may_surface` clears a ranked row by
|
|
160
|
+
# comparing the indexed `revision_id` against the live
|
|
161
|
+
# pointer, and a build that followed the corrupt pointer
|
|
162
|
+
# writes chunks the pointer *agrees* with -- so the withheld
|
|
163
|
+
# revision's body would rank and be excerpted under this
|
|
164
|
+
# item's approved status.
|
|
165
|
+
#
|
|
166
|
+
# Refused rather than skipped, and refused for the whole
|
|
167
|
+
# build: skipping would drop an approved document out of the
|
|
168
|
+
# index with nothing said, which reads as a relevance problem
|
|
169
|
+
# rather than as damage. `IndexBuilder.build` unlinks what
|
|
170
|
+
# this run wrote and `_run_build` converts this into
|
|
171
|
+
# `{error, remedy}` at exit 1, so nothing is published and the
|
|
172
|
+
# previous index still answers.
|
|
173
|
+
#
|
|
174
|
+
# Names no id and no cell: the message reaches the operator's
|
|
175
|
+
# terminal through `theurian index build`, and the id it would
|
|
176
|
+
# carry is the withheld item's.
|
|
177
|
+
msg = (
|
|
178
|
+
"This project's canonical state disagrees with its own records: an "
|
|
179
|
+
"item points at a revision that belongs to a different item, so this "
|
|
180
|
+
"build would index that revision's content under the wrong item."
|
|
181
|
+
)
|
|
182
|
+
raise InvariantViolationError(msg)
|
|
152
183
|
|
|
153
184
|
# The title is prepended to the body before splitting so that a
|
|
154
185
|
# query matching only the title still finds the document. A
|
|
@@ -78,6 +78,22 @@ ACTIVE_POINTER_REMEDY: Final = (
|
|
|
78
78
|
"the pointer is derived, so nothing is lost."
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
+
#: The cure for derived state that this installation did not build. Names the
|
|
82
|
+
#: whole `.theurian/state/` directory rather than the pointer alone: the
|
|
83
|
+
#: databases there are what carry the untrusted bytes, they are named by a hash
|
|
84
|
+
#: an attacker can compute, and `migrate apply` reuses a database file by name
|
|
85
|
+
#: (`database_for`) -- so deleting the pointer alone leaves the untrusted
|
|
86
|
+
#: database in place for the next apply to open. Also names the Git escape,
|
|
87
|
+
#: because the shape this closes is a repository contributor who force-added the
|
|
88
|
+
#: directory past its ADR-0004 ignore: a working-tree delete comes straight back
|
|
89
|
+
#: on the next checkout until the file is untracked.
|
|
90
|
+
UNBUILT_STATE_REMEDY: Final = (
|
|
91
|
+
"Delete .theurian/state/ and run `theurian migrate apply` (then "
|
|
92
|
+
"`theurian index build`) to rebuild it locally from the Git-tracked migrations. "
|
|
93
|
+
"If .theurian/state/ is tracked by Git, also run `git rm --cached -r .theurian/state`: "
|
|
94
|
+
"derived state must never be version-controlled (ADR-0004)."
|
|
95
|
+
)
|
|
96
|
+
|
|
81
97
|
#: The half of "rename a project" that is easy to omit and impossible to notice.
|
|
82
98
|
#: Canonical rows are stamped with the id in force at `migrate apply`, and
|
|
83
99
|
#: `migrate apply` is idempotent, so it will not restamp them. An id changed
|
|
@@ -1147,3 +1163,164 @@ class ProjectRegistry:
|
|
|
1147
1163
|
temporary = self.path.with_suffix(".json.tmp")
|
|
1148
1164
|
temporary.write_text(json.dumps(entries, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
1149
1165
|
os.replace(temporary, self.path) # noqa: PTH105 -- atomic replace
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
@dataclass(frozen=True, slots=True)
|
|
1169
|
+
class BuildProvenance:
|
|
1170
|
+
"""This installation's record of the derived state it built (ADR-0004, SEC-7).
|
|
1171
|
+
|
|
1172
|
+
The class this closes: everything under `.theurian/state/` -- the active
|
|
1173
|
+
pointers and the SQLite databases they name -- is derived and git-ignored
|
|
1174
|
+
(ADR-0004), but nothing stops a repository contributor from force-adding a
|
|
1175
|
+
doctored copy (`git add -f`, past the ignore) and a victim who clones (or
|
|
1176
|
+
downloads the ZIP/tarball) + `project register` + serves over MCP, *without
|
|
1177
|
+
ever running `migrate apply`*, from being served the attacker's bytes. The
|
|
1178
|
+
trust was filesystem presence: a database file whose name matched the
|
|
1179
|
+
pointer's hash was opened and read as authoritative.
|
|
1180
|
+
|
|
1181
|
+
Presence cannot be the discriminator, because `active.json`'s ``stateHash``
|
|
1182
|
+
binds the migration *set*, not the database bytes, and the database filename
|
|
1183
|
+
is derived from that hash (:meth:`StateHash.database_filename`). A
|
|
1184
|
+
self-consistent doctored pair -- status flipped, rows injected, every
|
|
1185
|
+
integrity record recomputed to match -- has no internal inconsistency to
|
|
1186
|
+
catch. The only thing an attacker who authored the repository cannot forge is
|
|
1187
|
+
whether *this installation* built the artifact, so provenance is recorded
|
|
1188
|
+
here, out of the repository tree, in ``THEURIAN_DATA_DIR`` beside the
|
|
1189
|
+
registry -- the one place a repository contributor cannot write to.
|
|
1190
|
+
|
|
1191
|
+
**Delivery-independent by construction.** The discriminator is "did this
|
|
1192
|
+
install build it", not "is it tracked by Git", so it refuses a clone, a ZIP
|
|
1193
|
+
download and a repackaged tarball alike -- a `git ls-files` probe would catch
|
|
1194
|
+
only the clone, since repackaging strips the tracking metadata and leaves the
|
|
1195
|
+
file present-but-untracked.
|
|
1196
|
+
|
|
1197
|
+
**Keyed by resolved root path, not project id.** The resolution layer that
|
|
1198
|
+
enforces the check holds the root (:attr:`ProjectPaths.root`) but not always
|
|
1199
|
+
the id; the root is the physical location the victim's own machine chose for
|
|
1200
|
+
the checkout; and two directories that would derive the same id from their
|
|
1201
|
+
name (:func:`derive_project_id` collides on directory name) stay distinct
|
|
1202
|
+
here.
|
|
1203
|
+
|
|
1204
|
+
**What it does not close, recorded rather than hidden.** The record vouches
|
|
1205
|
+
for a *hash*, not for the database bytes -- verifying bytes would mean hashing
|
|
1206
|
+
the whole database on every query. So an attacker who can replace a database
|
|
1207
|
+
*after* this install built the matching hash (a tracked sidecar overwriting a
|
|
1208
|
+
local build on the next `git pull`, or local filesystem write access) is out
|
|
1209
|
+
of scope for this control and left to the read-back integrity guards (#30
|
|
1210
|
+
PR2) and the schema-version and corruption checks. The primary vector -- a
|
|
1211
|
+
build this installation never produced -- is closed outright, because no
|
|
1212
|
+
record exists for it at all.
|
|
1213
|
+
"""
|
|
1214
|
+
|
|
1215
|
+
path: Path
|
|
1216
|
+
|
|
1217
|
+
@classmethod
|
|
1218
|
+
def default(cls, data_dir: Path | None = None) -> BuildProvenance:
|
|
1219
|
+
base = data_dir or Path(os.environ.get("THEURIAN_DATA_DIR", Path.home() / ".theurian"))
|
|
1220
|
+
return cls(path=base / "provenance.json")
|
|
1221
|
+
|
|
1222
|
+
@classmethod
|
|
1223
|
+
def for_registry(cls, registry: ProjectRegistry) -> BuildProvenance:
|
|
1224
|
+
"""The provenance store beside a registry, in the same data directory.
|
|
1225
|
+
|
|
1226
|
+
Derived from the registry rather than re-reading ``THEURIAN_DATA_DIR`` so
|
|
1227
|
+
the serve-side check reads exactly the directory the registry it was
|
|
1228
|
+
handed lives in, however that directory was resolved. The build side
|
|
1229
|
+
(``migrate apply``, ``index build``) reaches the same file through
|
|
1230
|
+
:meth:`default`, because both resolve the same environment variable.
|
|
1231
|
+
"""
|
|
1232
|
+
return cls(path=registry.path.parent / "provenance.json")
|
|
1233
|
+
|
|
1234
|
+
def _load(self) -> dict[str, Any]:
|
|
1235
|
+
"""The recorded builds, or an empty map on any failure to read them.
|
|
1236
|
+
|
|
1237
|
+
**Fail closed.** A provenance file this process cannot read or parse
|
|
1238
|
+
vouches for nothing, so every artifact is refused until a local build
|
|
1239
|
+
rewrites it. The file is derived and lives in the user's own data
|
|
1240
|
+
directory, so `migrate apply` is always the cure and losing it costs only
|
|
1241
|
+
a re-apply -- the same trade the registry and the state pointer make.
|
|
1242
|
+
"""
|
|
1243
|
+
if not self.path.exists():
|
|
1244
|
+
return {}
|
|
1245
|
+
try:
|
|
1246
|
+
loaded = json.loads(self.path.read_text(encoding="utf-8"))
|
|
1247
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
1248
|
+
return {}
|
|
1249
|
+
return loaded if isinstance(loaded, dict) else {}
|
|
1250
|
+
|
|
1251
|
+
def _built(self, raw: dict[str, Any], root: Path, kind: str) -> list[str]:
|
|
1252
|
+
entry = raw.get(str(root.resolve()))
|
|
1253
|
+
recorded = entry.get(kind) if isinstance(entry, dict) else None
|
|
1254
|
+
if not isinstance(recorded, list):
|
|
1255
|
+
return []
|
|
1256
|
+
return [value for value in recorded if isinstance(value, str)]
|
|
1257
|
+
|
|
1258
|
+
def has_state(self, root: Path, state_hash: str) -> bool:
|
|
1259
|
+
"""Whether this installation built the canonical state named by ``state_hash``."""
|
|
1260
|
+
return state_hash in self._built(self._load(), root, "state")
|
|
1261
|
+
|
|
1262
|
+
def has_index(self, root: Path, index_build_id: str) -> bool:
|
|
1263
|
+
"""Whether this installation built the retrieval index named by ``index_build_id``."""
|
|
1264
|
+
return index_build_id in self._built(self._load(), root, "index")
|
|
1265
|
+
|
|
1266
|
+
def record_state(self, root: Path, state_hash: str) -> None:
|
|
1267
|
+
"""Record that this installation built the canonical state ``state_hash``."""
|
|
1268
|
+
self._record(root, "state", state_hash)
|
|
1269
|
+
|
|
1270
|
+
def record_index(self, root: Path, index_build_id: str) -> None:
|
|
1271
|
+
"""Record that this installation built the retrieval index ``index_build_id``."""
|
|
1272
|
+
self._record(root, "index", index_build_id)
|
|
1273
|
+
|
|
1274
|
+
def _record(self, root: Path, kind: str, value: str) -> None:
|
|
1275
|
+
"""Append one built artifact to a root's record, atomically.
|
|
1276
|
+
|
|
1277
|
+
Accumulates rather than replaces: a prior build's state may still be
|
|
1278
|
+
served (a pinned snapshot, a not-yet-collected build), so the record
|
|
1279
|
+
keeps every hash this installation produced rather than only the latest.
|
|
1280
|
+
Read-modify-write with an :func:`os.replace` swap, the same discipline as
|
|
1281
|
+
the registry; a lost update under concurrent writers drops a hash and so
|
|
1282
|
+
fails closed -- the artifact is refused until the next build re-records
|
|
1283
|
+
it -- rather than vouching for one this installation did not build.
|
|
1284
|
+
"""
|
|
1285
|
+
raw = self._load()
|
|
1286
|
+
key = str(root.resolve())
|
|
1287
|
+
entry_value = raw.get(key)
|
|
1288
|
+
entry = dict(entry_value) if isinstance(entry_value, dict) else {}
|
|
1289
|
+
built = self._built(raw, root, kind)
|
|
1290
|
+
if value not in built:
|
|
1291
|
+
built = [*built, value]
|
|
1292
|
+
entry[kind] = built
|
|
1293
|
+
updated = dict(raw)
|
|
1294
|
+
updated[key] = entry
|
|
1295
|
+
self._write(updated)
|
|
1296
|
+
|
|
1297
|
+
def _write(self, entries: dict[str, Any]) -> None:
|
|
1298
|
+
self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
1299
|
+
temporary = self.path.with_suffix(".json.tmp")
|
|
1300
|
+
temporary.write_text(json.dumps(entries, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
1301
|
+
os.replace(temporary, self.path) # noqa: PTH105 -- atomic replace
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def verify_state_provenance(
|
|
1305
|
+
paths: ProjectPaths, active: ActiveState, provenance: BuildProvenance
|
|
1306
|
+
) -> None:
|
|
1307
|
+
"""Refuse canonical state this installation did not build (ADR-0004, SEC-7).
|
|
1308
|
+
|
|
1309
|
+
The enforcement point for :class:`BuildProvenance`. Called on the serve path
|
|
1310
|
+
(the MCP tools' ``_resolve``) and before an index is built from canonical
|
|
1311
|
+
state (``theurian index build``), so a database that this installation never
|
|
1312
|
+
produced never influences a served result -- whatever put it on disk.
|
|
1313
|
+
|
|
1314
|
+
Raises:
|
|
1315
|
+
ProjectError: If no out-of-tree record shows this installation built the
|
|
1316
|
+
state the in-tree pointer names. Carries :data:`UNBUILT_STATE_REMEDY`;
|
|
1317
|
+
quotes no cell content, only the state directory's own path.
|
|
1318
|
+
"""
|
|
1319
|
+
if not provenance.has_state(paths.root, str(active.state_hash)):
|
|
1320
|
+
raise ProjectError(
|
|
1321
|
+
f"The derived knowledge state under {paths.state} was not built by this "
|
|
1322
|
+
f"Theurian installation, so it will not be served. It was delivered with the "
|
|
1323
|
+
f"project rather than rebuilt here from the Git-tracked migrations, which is "
|
|
1324
|
+
f"exactly what an untrusted repository must not be able to do (ADR-0004).",
|
|
1325
|
+
remedy=UNBUILT_STATE_REMEDY,
|
|
1326
|
+
)
|
|
@@ -1018,8 +1018,11 @@ class ResultGate:
|
|
|
1018
1018
|
if item is None or revision is None: # pragma: no cover - a foreign key holds this
|
|
1019
1019
|
# `visible` cleared this candidate moments ago, in this session,
|
|
1020
1020
|
# against an item whose `current_revision_id` names this revision
|
|
1021
|
-
# under a foreign key
|
|
1022
|
-
#
|
|
1021
|
+
# under a foreign key -- composite over `(project_id,
|
|
1022
|
+
# revision_id)` since #24, so the project-scoped read below cannot
|
|
1023
|
+
# miss a revision the constraint calls present. Reaching here
|
|
1024
|
+
# means the state database disagrees with itself, which is not
|
|
1025
|
+
# something to answer around:
|
|
1023
1026
|
# a silently shorter answer would be indistinguishable from "we
|
|
1024
1027
|
# have no such decision".
|
|
1025
1028
|
msg = (
|
|
@@ -129,6 +129,16 @@ NO_PUBLISHED_INDEX: str = "no-published-index"
|
|
|
129
129
|
INDEX_UNUSABLE: str = "index-unusable"
|
|
130
130
|
NOTHING_TO_PURGE: str = "nothing-to-purge"
|
|
131
131
|
|
|
132
|
+
#: The published build a purge would copy forward was not produced by this
|
|
133
|
+
#: installation, so the composition root declines to purge it: copying an
|
|
134
|
+
#: unprovenanced build's surviving rows into a fresh build and recording that
|
|
135
|
+
#: build would launder a committed, doctored index into a provenanced one the
|
|
136
|
+
#: serve path trusts. Reported (not raised) so the skip is visible, and named
|
|
137
|
+
#: here because it is part of the ``reason`` vocabulary a command reports even
|
|
138
|
+
#: though the gate that produces it lives at the composition root (ADR-0004,
|
|
139
|
+
#: SEC-7).
|
|
140
|
+
UNTRUSTED_SOURCE_INDEX: str = "untrusted-source-index"
|
|
141
|
+
|
|
132
142
|
#: What an operator does when the purge itself failed. The index is derived
|
|
133
143
|
#: (ADR-0004), so the cure is always a rebuild -- and it is the load-bearing half
|
|
134
144
|
#: of the failure report, because the current build still holds the withdrawn
|
|
@@ -480,6 +490,7 @@ __all__ = [
|
|
|
480
490
|
"NO_PUBLISHED_INDEX",
|
|
481
491
|
"NO_WITHDRAWAL",
|
|
482
492
|
"PURGE_FAILED_REMEDY",
|
|
493
|
+
"UNTRUSTED_SOURCE_INDEX",
|
|
483
494
|
"ForestRecomputeStore",
|
|
484
495
|
"PurgeableIndex",
|
|
485
496
|
"WithdrawalPurge",
|