theurian 0.1.0.dev5__tar.gz → 0.1.0.dev7__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.dev5 → theurian-0.1.0.dev7}/CHANGELOG.md +96 -3
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/PKG-INFO +1 -1
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/pyproject.toml +1 -1
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/__init__.py +1 -1
- theurian-0.1.0.dev7/src/theurian/application/migration_alias_guards.py +183 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/migration_engine.py +14 -5
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/proposal_service.py +20 -4
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/commands.py +55 -8
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/propose_commands.py +180 -6
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/errors.py +46 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/migration.py +12 -4
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/canonical_store.py +16 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/migration_loader.py +6 -4
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/store.py +18 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/tools.py +28 -12
- theurian-0.1.0.dev7/tests/integration/test_alias_item_id_collision.py +687 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_proposal_service.py +4 -2
- theurian-0.1.0.dev7/tests/integration/test_propose_cli.py +989 -0
- theurian-0.1.0.dev7/tests/unit/test_migration_alias_guards.py +183 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_result_gate_session.py +13 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_retrieval_depth.py +5 -0
- theurian-0.1.0.dev5/tests/integration/test_propose_cli.py +0 -386
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/.gitignore +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/LICENSE +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/README.md +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/hatch_build.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/forest_builder.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/index_builder.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/ingestion_service.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/migration_body_guards.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/project_service.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/retrieval_service.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_context.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_service.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_steps.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_withholding.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/visibility.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/withdrawal_purge.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/auth_commands.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/context.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/index_commands.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/main.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/setup_commands.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/instance.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/runner.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/server.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/chunking.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/compatibility.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/context.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/enums.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/extras.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/identifiers.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ingestion.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/knowledge.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/authorization.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/daemon_manager.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/determinism.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/embedding.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/index_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/mcp_client_config.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/object_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/reranking.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/review_provider.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/secret_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/source_parser.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/specification_provider.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/summarization.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/vector_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/project.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/proposal.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ranking.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/raptor.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/retrieval.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/review.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/setup.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/specification.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/state.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/values.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/indexing/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/claude/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/claude/mcp_config.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/determinism.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/embedding/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/embedding/hashing.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/markdown.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/openapi.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/registry.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/structured.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/git/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/github/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/raptor/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/raptor/extractive.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/secrets/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/secrets/file_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/launchagent.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/runner.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/systemd_user.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/connection.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_forest.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_purge.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_query.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_scan.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_schema.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/schema.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/vector/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/ingestion/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/results.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/search.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/migrations/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/normalization/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/normalization/projection.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/observability/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/retrieval/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/review/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/env_file.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/paths.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/tokens.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/yaml_loading.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/specification/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/traceability/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/command_extraction.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/command_population.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/conftest.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/__init__.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/clock.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/ids.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/pages.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/setup.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_absence_proof.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_auth_rotate.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_bare_install.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_canonical_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_canonical_store_corruption.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_claude_mcp_config.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_cli_commands.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_cli_help_without_rich.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_daemon.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_builder.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_builder_scale.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_node_scope.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_purge_equality.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_purge_recompute.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_retrieval.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_store_retrieval.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_gc_during_a_search.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_fallback.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_gc_cli.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_purge.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_purge_differential.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_purge_nodes.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_scan_fold.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_schema_v4.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_store.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_ingestion.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_init_gitignore_block.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_mcp_tools.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_project_registry.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_retrieval_service.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_revision_id_reuse.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_scan_exhaustion.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_service_adapters.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_session_start_hook.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_cli.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_env_file.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_journal.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_partial_failure.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_probe_assertions.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_report_withholding.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_service.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_short_query_retrieval.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_state_provenance.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_unreadable_registry_surface.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_wire_contract.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_withdrawal_purge.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_writer_contract.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_artifact_integrity_claim.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_candidate_cut.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_candidate_depth.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_chunking.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_cli_help_rendering.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_command_extraction.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_compatibility.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_config_key_call_sites.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_daemon_extra.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_documented_commands.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_domain_invariants.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_env_file_merge.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_examples.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_extractive_summarizer.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_forest_derivation.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_forest_fanout.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_gate_call_sites.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_hashing_embedding.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ingest_manifest_path.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_install_claims.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_layering.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_migration_engine.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_migration_loader_errors.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_migration_loader_identity.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_network_call_sites.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_parsers.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_path_security.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_plugin_boundary.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ports.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_project_and_traceability.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_project_registry_errors.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_projection.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_proposal.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ranking.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_raptor_scope.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ref_recording.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_resolve_context_call_sites.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_result_payload.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_schemas.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_scope_isolation.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_secret_detector.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_setup_changed_paths.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_setup_claims.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_setup_domain.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_state_hash.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_test_fixtures.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_tokens.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_yaml_loading.py +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/README.md +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/cli/version.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/config/project-config.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/knowledge/retrieval-result.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/knowledge-search-response.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/knowledge-status-response.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/project-list-response.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/retrieval-metadata.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/tool-context.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/migrations/migration.schema.json +0 -0
- {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/protocol/compatibility.schema.json +0 -0
|
@@ -12,6 +12,99 @@ 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.dev7] - 2026-08-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`theurian propose` can now set `--scope-path` and `--label`**
|
|
20
|
+
([#249](https://github.com/theurian/theurian/issues/249)). Each is repeatable
|
|
21
|
+
and keeps the order it was given: `--scope-path` writes `metadata.scope.paths`
|
|
22
|
+
(the globs Milestone 8's drift detection will read), and `--label` writes
|
|
23
|
+
`metadata.labels`. A `--label` given alongside `--authored-here` is
|
|
24
|
+
de-duplicated against the implied `authored-in-theurian` label — the schema
|
|
25
|
+
declares `labels` `uniqueItems`, so a duplicate would otherwise turn a legal
|
|
26
|
+
invocation into a refusal. An enhancement rather than a fix: before this, these
|
|
27
|
+
fields were simply unreachable from the propose flow, and their absence
|
|
28
|
+
published nothing false.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **A maintainer-reviewed, public ADR accepted through `propose` → `accept`
|
|
33
|
+
published `trustLevel: unverified` and `sensitivity: internal` on every result**
|
|
34
|
+
([#249](https://github.com/theurian/theurian/issues/249)). `theurian propose`
|
|
35
|
+
could express no `trustLevel` or `sensitivity`, so a corpus built by the shipped
|
|
36
|
+
flow could only ever carry the schema defaults; the loader completed the omitted
|
|
37
|
+
fields silently, and both reach every `knowledge.search` and `knowledge.get`
|
|
38
|
+
result. Measured on the first dogfooding slice: three publicly readable ADRs a
|
|
39
|
+
maintainer had merged were published as `unverified`/`internal` beside
|
|
40
|
+
`status: approved`, and hand-editing the generated YAML was the only remedy.
|
|
41
|
+
`propose` now takes `--trust-level {unverified|inferred|reviewed|authoritative}`
|
|
42
|
+
and `--sensitivity {public|internal|confidential|restricted}`, on both the
|
|
43
|
+
create and update flows, so a false value is correctable rather than permanent.
|
|
44
|
+
When either is omitted the default is not stamped into the migration — that
|
|
45
|
+
would assert a judgement the caller never made — but is named in the draft's
|
|
46
|
+
`--json` next steps, so an omission is a surfaced choice rather than a silent
|
|
47
|
+
false positive.
|
|
48
|
+
|
|
49
|
+
## [0.1.0.dev6] - 2026-08-19
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A rejected item's relation note reached an approved item's `knowledge.get`
|
|
54
|
+
through an alias-id collision**
|
|
55
|
+
([GHSA-vx8x-rjfj-9x54](https://github.com/theurian/theurian/security/advisories/GHSA-vx8x-rjfj-9x54)).
|
|
56
|
+
Critical; affected 0.1.0.dev0–0.1.0.dev5. An `addAlias` whose key
|
|
57
|
+
equalled the id of a live `rejected` item, while the alias pointed at an
|
|
58
|
+
approved item, defeated the relation-visibility gate: `get_item` resolves an
|
|
59
|
+
alias before it looks up a status, so a lookup for the retired id resolved to
|
|
60
|
+
the approved target and cleared the gate as that item. An incoming edge the
|
|
61
|
+
rejected item authored — for example `contradicts` — and its `note`, where the
|
|
62
|
+
secret that caused the rejection lives, was published on the *approved* item's
|
|
63
|
+
response. The withheld id itself never appeared; the note did. Requesting the
|
|
64
|
+
withheld id directly was still correctly refused, and the ranked
|
|
65
|
+
`knowledge.search` path was never affected — it is held by the revision-identity
|
|
66
|
+
check that T-18 underwrites.
|
|
67
|
+
|
|
68
|
+
The read side is fixed independently of the write-side refusal below, because a
|
|
69
|
+
migration guard cannot reach a database an affected version already built:
|
|
70
|
+
`_relation_is_visible` now reads each relation endpoint's status through a new
|
|
71
|
+
non-resolving `get_item_exact`, the row the id literally names. Reachability
|
|
72
|
+
still resolves an alias — `knowledge.get(old)` reaches `new` after a rename —
|
|
73
|
+
but authority, a visibility decision on a referenced id, reads the
|
|
74
|
+
literally-named row. See
|
|
75
|
+
[T-21 in the threat model](../../docs/security/threat-model.md).
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- **BREAKING — an alias key may not collide with a non-deprecated item id across a
|
|
80
|
+
migration set**
|
|
81
|
+
([GHSA-vx8x-rjfj-9x54](https://github.com/theurian/theurian/security/advisories/GHSA-vx8x-rjfj-9x54)).
|
|
82
|
+
|
|
83
|
+
**Old shape:** a set whose `addAlias` key equalled a live item's id applied
|
|
84
|
+
silently. With that item `rejected` and the alias pointing at an approved item,
|
|
85
|
+
the approved item's `knowledge.get` then published the rejected item's edge and
|
|
86
|
+
note (the Fixed entry above).
|
|
87
|
+
|
|
88
|
+
**New shape:** `migrate validate` and `migrate apply` both refuse the whole set
|
|
89
|
+
with `AliasItemCollisionError` at exit 4, naming the alias, the item it points
|
|
90
|
+
at, and the item's final status, and quoting no note. `apply` refuses before it
|
|
91
|
+
creates a database file, so a refused set costs no state — the property issue
|
|
92
|
+
#63's refusal already has. Both collision directions are refused — an `addAlias`
|
|
93
|
+
authored over an existing item, and a `createItem` that takes an id an alias
|
|
94
|
+
already keys — and a collision that straddles an already-applied migration is
|
|
95
|
+
caught too, because `apply` reloads every migration file into the set the guard
|
|
96
|
+
sees. The one exempt shape is the rename `deprecateItem(old)` then
|
|
97
|
+
`addAlias(old -> new)`, which leaves `old` `deprecated`; every other final
|
|
98
|
+
status is refused, `superseded` included. `migrate status` does not refuse — its
|
|
99
|
+
contract is observation — but names every colliding migration under
|
|
100
|
+
`refusedIds`, matching how it treats the tenant/ACL and duplicate-body rules.
|
|
101
|
+
|
|
102
|
+
Breaking because a migration set that applied on `0.1.0.dev5` and earlier now
|
|
103
|
+
refuses. No stable release exists — the published versions are `0.1.0.devN` —
|
|
104
|
+
and no compatibility promise covers it, but the break is named here rather than
|
|
105
|
+
filed as a fix. Documented in
|
|
106
|
+
[`docs/protocol/migrations.md`](../../docs/protocol/migrations.md).
|
|
107
|
+
|
|
15
108
|
## [0.1.0.dev5] - 2026-08-19
|
|
16
109
|
|
|
17
110
|
### Added
|
|
@@ -65,7 +158,8 @@ Pre-1.0, a MINOR bump may change the protocol. Post-1.0, only a MAJOR may.
|
|
|
65
158
|
### Changed
|
|
66
159
|
|
|
67
160
|
- **BREAKING — one body file may back only one revision across a migration set**
|
|
68
|
-
([#210](https://github.com/theurian/theurian/issues/210)
|
|
161
|
+
([#210](https://github.com/theurian/theurian/issues/210);
|
|
162
|
+
[GHSA-w5cm-cqf9-vm7r](https://github.com/theurian/theurian/security/advisories/GHSA-w5cm-cqf9-vm7r)).
|
|
69
163
|
|
|
70
164
|
**Old shape:** a set in which two *different* revisions named one `contentFile`
|
|
71
165
|
applied. Measured: two hand-written migrations sharing one path, with a correct
|
|
@@ -978,8 +1072,7 @@ Pre-1.0, a MINOR bump may change the protocol. Post-1.0, only a MAJOR may.
|
|
|
978
1072
|
matching hash is out of scope for this control and left to the schema gate, the
|
|
979
1073
|
#30 read-back guards, and the corruption checks.
|
|
980
1074
|
|
|
981
|
-
|
|
982
|
-
advisory ships; embargoed until then. -->
|
|
1075
|
+
Published as [GHSA-266v-fcj2-qggx](https://github.com/theurian/theurian/security/advisories/GHSA-266v-fcj2-qggx).
|
|
983
1076
|
|
|
984
1077
|
- **`knowledge.search` gains one `fallbackReason`, `index-unbuilt`**, emitted when
|
|
985
1078
|
the published index was not built by this installation and the ranked path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: theurian
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev7
|
|
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.dev7"
|
|
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.dev7"
|
|
12
12
|
__protocol_version__ = CURRENT_PROTOCOL_VERSION
|
|
13
13
|
|
|
14
14
|
__all__ = ["__protocol_version__", "__version__"]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Alias/item-id collision guard over a migration set (SEC-13, T-21).
|
|
2
|
+
|
|
3
|
+
A static, whole-set check that no ``addAlias`` key equals the id of an item whose
|
|
4
|
+
final status is anything but ``deprecated``. Pure function of a ``MigrationSet``:
|
|
5
|
+
it touches no store, opens no transaction, and reads nothing from disk.
|
|
6
|
+
``MigrationEngine.apply`` calls :func:`refuse_alias_item_id_collision`; ``migrate
|
|
7
|
+
validate``/``apply`` call it through the CLI before any state is written, and
|
|
8
|
+
``migrate status`` reports it through :func:`alias_item_collision_violations`.
|
|
9
|
+
|
|
10
|
+
The collision is the T-21 disclosure's write-side cause.
|
|
11
|
+
``SqliteCanonicalStore`` resolves an alias before it looks up a status, so an
|
|
12
|
+
``addAlias`` key equal to a ``rejected`` item's id resolves to the approved item
|
|
13
|
+
the alias points at and clears a relation-visibility gate as that item --
|
|
14
|
+
publishing the rejected item's edge and its ``note`` (where the secret that
|
|
15
|
+
caused the rejection lives) on the approved item's response. The read side is
|
|
16
|
+
fixed independently (``_relation_is_visible`` reads the literally-named row via
|
|
17
|
+
``get_item_exact``); this refuses the collision from ever being authored.
|
|
18
|
+
|
|
19
|
+
Whole-set rather than pending-only, for the reason
|
|
20
|
+
:func:`~theurian.application.migration_engine.refuse_unenforceable_scope` is:
|
|
21
|
+
``migrate validate`` holds no store and so has no notion of pending, and the two
|
|
22
|
+
commands must decide a statically decidable rule on identical input or reopen
|
|
23
|
+
issue #36's class. It also covers the cross-set collision without a store read,
|
|
24
|
+
because ``migrate apply`` reloads every migration file -- an item created by an
|
|
25
|
+
earlier applied migration is in the set the guard sees.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Iterator
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
|
|
33
|
+
from theurian.domain.enums import KnowledgeStatus
|
|
34
|
+
from theurian.domain.errors import AliasItemCollisionError
|
|
35
|
+
from theurian.domain.identifiers import ItemId, MigrationId
|
|
36
|
+
from theurian.domain.migration import (
|
|
37
|
+
AddAlias,
|
|
38
|
+
CreateItem,
|
|
39
|
+
DeprecateItem,
|
|
40
|
+
MigrationSet,
|
|
41
|
+
RemoveAlias,
|
|
42
|
+
RestoreItem,
|
|
43
|
+
UpsertRevision,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _final_item_statuses(migration_set: MigrationSet) -> dict[ItemId, KnowledgeStatus]:
|
|
48
|
+
"""Each item id's status after the whole set, computed in application order.
|
|
49
|
+
|
|
50
|
+
"Final" is well defined only in application (topological) order, so this
|
|
51
|
+
normalises to it with ``MigrationSet.ordered`` rather than trusting the
|
|
52
|
+
caller's iteration order: a raw ``MigrationSet(tuple(...))`` whose
|
|
53
|
+
``deprecateItem`` merely lands last would otherwise read as ``deprecated`` and
|
|
54
|
+
defeat the T-21 exemption. ``ordered`` is idempotent for an already-ordered
|
|
55
|
+
set, so the ordered-loader caller pays only a re-sort, and re-validates a
|
|
56
|
+
cycle/missing-dep an applyable set has already passed.
|
|
57
|
+
|
|
58
|
+
Only the four operations that move an item's status are consulted, and the
|
|
59
|
+
same way ``MigrationEngine`` moves it: ``createItem`` sets ``draft`` for a
|
|
60
|
+
*new* id (a re-create of an existing one is a no-op, so ``setdefault``),
|
|
61
|
+
``upsertRevision`` sets the revision's declared status, ``deprecateItem`` sets
|
|
62
|
+
``deprecated``, ``restoreItem`` sets ``approved``. ``changeSensitivity`` and
|
|
63
|
+
``changeOwner`` preserve status (the engine's ``_replace_item`` does), so they
|
|
64
|
+
are not consulted.
|
|
65
|
+
"""
|
|
66
|
+
statuses: dict[ItemId, KnowledgeStatus] = {}
|
|
67
|
+
for migration in MigrationSet.ordered(migration_set.migrations):
|
|
68
|
+
for operation in migration.operations:
|
|
69
|
+
match operation:
|
|
70
|
+
case CreateItem():
|
|
71
|
+
statuses.setdefault(operation.item_id, KnowledgeStatus.DRAFT)
|
|
72
|
+
case UpsertRevision():
|
|
73
|
+
statuses[operation.item_id] = operation.metadata.status
|
|
74
|
+
case DeprecateItem():
|
|
75
|
+
statuses[operation.item_id] = KnowledgeStatus.DEPRECATED
|
|
76
|
+
case RestoreItem():
|
|
77
|
+
statuses[operation.item_id] = KnowledgeStatus.APPROVED
|
|
78
|
+
return statuses
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class _AliasTarget:
|
|
83
|
+
"""Where a live alias key points, and the migration that set it."""
|
|
84
|
+
|
|
85
|
+
item_id: ItemId
|
|
86
|
+
migration_id: MigrationId
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _final_alias_targets(migration_set: MigrationSet) -> dict[ItemId, _AliasTarget]:
|
|
90
|
+
"""Each alias key still live after the whole set, and where it was set.
|
|
91
|
+
|
|
92
|
+
Normalised to application order via ``MigrationSet.ordered`` for the same
|
|
93
|
+
reason :func:`_final_item_statuses` is: whether a key added then removed on
|
|
94
|
+
two migrations is still live, and which migration id a refusal names, are
|
|
95
|
+
last-write-wins over application order, not over the caller's tuple order.
|
|
96
|
+
|
|
97
|
+
``addAlias`` records a key; ``removeAlias`` takes one back, so a key added and
|
|
98
|
+
later removed is not a live collision and is dropped. The migration id kept is
|
|
99
|
+
the ``addAlias``'s own, so a refusal names the operation an author edits.
|
|
100
|
+
"""
|
|
101
|
+
targets: dict[ItemId, _AliasTarget] = {}
|
|
102
|
+
for migration in MigrationSet.ordered(migration_set.migrations):
|
|
103
|
+
for operation in migration.operations:
|
|
104
|
+
match operation:
|
|
105
|
+
case AddAlias():
|
|
106
|
+
targets[operation.alias] = _AliasTarget(
|
|
107
|
+
operation.item_id, migration.migration_id
|
|
108
|
+
)
|
|
109
|
+
case RemoveAlias():
|
|
110
|
+
targets.pop(operation.alias, None)
|
|
111
|
+
return targets
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class _AliasCollision:
|
|
116
|
+
"""One alias key that also names a live, non-deprecated item."""
|
|
117
|
+
|
|
118
|
+
alias: ItemId
|
|
119
|
+
target: ItemId
|
|
120
|
+
status: KnowledgeStatus
|
|
121
|
+
migration_id: MigrationId
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _alias_item_collisions(migration_set: MigrationSet) -> Iterator[_AliasCollision]:
|
|
125
|
+
"""Every live alias key that also names an item whose final status is not deprecated.
|
|
126
|
+
|
|
127
|
+
Both collision directions are one predicate: the shape is always "an alias key
|
|
128
|
+
equals an item id", whether the ``addAlias`` was authored over an existing
|
|
129
|
+
item or a ``createItem`` later took an id an alias already keyed. Yielded in
|
|
130
|
+
sorted alias-key order, so the throwing guard refuses a deterministic first
|
|
131
|
+
one when a set carries several.
|
|
132
|
+
"""
|
|
133
|
+
statuses = _final_item_statuses(migration_set)
|
|
134
|
+
targets = _final_alias_targets(migration_set)
|
|
135
|
+
for alias in sorted(targets, key=lambda a: a.value):
|
|
136
|
+
status = statuses.get(alias)
|
|
137
|
+
if status is not None and status is not KnowledgeStatus.DEPRECATED:
|
|
138
|
+
target = targets[alias]
|
|
139
|
+
yield _AliasCollision(
|
|
140
|
+
alias=alias,
|
|
141
|
+
target=target.item_id,
|
|
142
|
+
status=status,
|
|
143
|
+
migration_id=target.migration_id,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def refuse_alias_item_id_collision(migration_set: MigrationSet) -> None:
|
|
148
|
+
"""Refuse a set whose alias key collides with a live, non-deprecated item id (T-21).
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
AliasItemCollisionError: On the first colliding alias key in sorted order.
|
|
152
|
+
"""
|
|
153
|
+
for collision in _alias_item_collisions(migration_set):
|
|
154
|
+
raise AliasItemCollisionError(
|
|
155
|
+
alias=collision.alias,
|
|
156
|
+
alias_target=collision.target,
|
|
157
|
+
item_status=collision.status.value,
|
|
158
|
+
migration_id=collision.migration_id,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def alias_item_collision_violations(migration_set: MigrationSet) -> tuple[MigrationId, ...]:
|
|
163
|
+
"""Every migration :func:`refuse_alias_item_id_collision` would refuse, without raising.
|
|
164
|
+
|
|
165
|
+
The non-throwing enumerator ``migrate status`` needs, the sibling of
|
|
166
|
+
``unenforceable_scope_violations`` and ``duplicate_content_file_violations``.
|
|
167
|
+
``status`` reports rather than gates (issue #63's MEDIUM-3), so this
|
|
168
|
+
statically decidable rule must stay visible there under ``refusedIds`` --
|
|
169
|
+
otherwise ``status`` would report ``refusedIds: []`` for a set
|
|
170
|
+
``validate``/``apply`` exit 4 on, the #210 gap. Reports each colliding
|
|
171
|
+
``addAlias``'s migration id, at most once, in sorted alias-key order.
|
|
172
|
+
"""
|
|
173
|
+
refused: list[MigrationId] = []
|
|
174
|
+
for collision in _alias_item_collisions(migration_set):
|
|
175
|
+
if collision.migration_id not in refused:
|
|
176
|
+
refused.append(collision.migration_id)
|
|
177
|
+
return tuple(refused)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = [
|
|
181
|
+
"alias_item_collision_violations",
|
|
182
|
+
"refuse_alias_item_id_collision",
|
|
183
|
+
]
|
|
@@ -20,6 +20,7 @@ from dataclasses import dataclass, field
|
|
|
20
20
|
from datetime import datetime
|
|
21
21
|
from typing import Final, Protocol
|
|
22
22
|
|
|
23
|
+
from theurian.application.migration_alias_guards import refuse_alias_item_id_collision
|
|
23
24
|
from theurian.application.migration_body_guards import refuse_duplicate_content_files
|
|
24
25
|
from theurian.domain.enums import KnowledgeStatus, RelationType, may_surface
|
|
25
26
|
from theurian.domain.errors import (
|
|
@@ -332,15 +333,23 @@ class MigrationEngine:
|
|
|
332
333
|
planning but before any write, so it never depends on what
|
|
333
334
|
has already been written.
|
|
334
335
|
DuplicateContentFileError: If two operations reference one body
|
|
335
|
-
file (issue #210). Checked
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
file (issue #210). Checked over the same whole set: the scope
|
|
337
|
+
refusal above names a specific migration as wrong, while this
|
|
338
|
+
one is a statement about the set, and reporting the narrower
|
|
339
|
+
fault first is what keeps a reader from being sent to a second
|
|
340
|
+
migration that is not the one to edit.
|
|
341
|
+
AliasItemCollisionError: If an ``addAlias`` key equals the id of an
|
|
342
|
+
item whose final status is not ``deprecated`` (SEC-13, T-21).
|
|
343
|
+
Checked last and whole-set: a store that resolves the alias
|
|
344
|
+
before a status lookup would let the retired item surface under
|
|
345
|
+
the approved item the alias points at. The CLI refuses this
|
|
346
|
+
before ``create_database`` too, so a refused apply leaves no
|
|
347
|
+
state database behind.
|
|
340
348
|
"""
|
|
341
349
|
plan = self.plan(writer, project_id, migration_set)
|
|
342
350
|
refuse_unenforceable_scope(migration_set)
|
|
343
351
|
refuse_duplicate_content_files(migration_set)
|
|
352
|
+
refuse_alias_item_id_collision(migration_set)
|
|
344
353
|
report = ApplyReport(skipped=list(plan.already_applied))
|
|
345
354
|
|
|
346
355
|
# The items whose surfaceability or current revision an operation could
|
|
@@ -46,7 +46,7 @@ from typing import Final
|
|
|
46
46
|
import yaml
|
|
47
47
|
|
|
48
48
|
from theurian.application.project_service import ProjectPaths
|
|
49
|
-
from theurian.domain.enums import KnowledgeKind, KnowledgeStatus
|
|
49
|
+
from theurian.domain.enums import KnowledgeKind, KnowledgeStatus, Sensitivity, TrustLevel
|
|
50
50
|
from theurian.domain.errors import PathEscapeError, TheurianError
|
|
51
51
|
from theurian.domain.identifiers import ItemId, MigrationId, ProposalId, RevisionId
|
|
52
52
|
from theurian.domain.knowledge import AUTHORED_IN_THEURIAN, SourceAnchor
|
|
@@ -131,6 +131,13 @@ class ProposalRequest:
|
|
|
131
131
|
evidence: Evidence
|
|
132
132
|
source_anchors: tuple[SourceAnchor, ...] = ()
|
|
133
133
|
labels: tuple[str, ...] = ()
|
|
134
|
+
scope_paths: tuple[str, ...] = ()
|
|
135
|
+
#: Absent means "not stated": the field is left out of the migration and the
|
|
136
|
+
#: loader applies the schema default. A value is never invented here, because
|
|
137
|
+
#: `unverified`/`internal` written into every draft would assert a judgement
|
|
138
|
+
#: the caller did not make (#249).
|
|
139
|
+
trust_level: TrustLevel | None = None
|
|
140
|
+
sensitivity: Sensitivity | None = None
|
|
134
141
|
namespace: str | None = None
|
|
135
142
|
expected_revision: RevisionId | None = None
|
|
136
143
|
|
|
@@ -840,9 +847,12 @@ def _migration_document( # noqa: PLR0913 -- the fields a migration has; all key
|
|
|
840
847
|
``status: approved`` is right even though nobody has approved it yet: the
|
|
841
848
|
file applies only once a human has merged it, and ``draft`` would land
|
|
842
849
|
knowledge that ``theurian index build`` leaves out. ``trustLevel`` and
|
|
843
|
-
``sensitivity`` are
|
|
844
|
-
|
|
845
|
-
|
|
850
|
+
``sensitivity`` are written only when the caller set them (``--trust-level``,
|
|
851
|
+
``--sensitivity``): left unset they are absent from the file and the loader
|
|
852
|
+
applies its defaults, ``unverified`` and ``internal``. Stamping those
|
|
853
|
+
defaults in unasked-for would assert a judgement the caller never made, so
|
|
854
|
+
the omission is surfaced in the CLI's next steps instead of written here
|
|
855
|
+
(#249).
|
|
846
856
|
"""
|
|
847
857
|
metadata: dict[str, object] = {
|
|
848
858
|
"title": request.title,
|
|
@@ -852,8 +862,14 @@ def _migration_document( # noqa: PLR0913 -- the fields a migration has; all key
|
|
|
852
862
|
"status": KnowledgeStatus.APPROVED.value,
|
|
853
863
|
"owner": request.owner,
|
|
854
864
|
}
|
|
865
|
+
if request.trust_level is not None:
|
|
866
|
+
metadata["trustLevel"] = request.trust_level.value
|
|
867
|
+
if request.sensitivity is not None:
|
|
868
|
+
metadata["sensitivity"] = request.sensitivity.value
|
|
855
869
|
if request.labels:
|
|
856
870
|
metadata["labels"] = list(request.labels)
|
|
871
|
+
if request.scope_paths:
|
|
872
|
+
metadata["scope"] = {"paths": list(request.scope_paths)}
|
|
857
873
|
if request.source_anchors:
|
|
858
874
|
metadata["sourceAnchors"] = [_anchor_document(a) for a in request.source_anchors]
|
|
859
875
|
|
|
@@ -21,6 +21,10 @@ from theurian.application.ingestion_service import (
|
|
|
21
21
|
IngestionService,
|
|
22
22
|
manifest_from,
|
|
23
23
|
)
|
|
24
|
+
from theurian.application.migration_alias_guards import (
|
|
25
|
+
alias_item_collision_violations,
|
|
26
|
+
refuse_alias_item_id_collision,
|
|
27
|
+
)
|
|
24
28
|
from theurian.application.migration_body_guards import (
|
|
25
29
|
duplicate_content_file_violations,
|
|
26
30
|
refuse_duplicate_content_files,
|
|
@@ -59,6 +63,7 @@ from theurian.cli.context import (
|
|
|
59
63
|
resolve_context,
|
|
60
64
|
)
|
|
61
65
|
from theurian.domain.errors import (
|
|
66
|
+
AliasItemCollisionError,
|
|
62
67
|
DuplicateContentFileError,
|
|
63
68
|
MigrationChecksumMismatchError,
|
|
64
69
|
MigrationCycleError,
|
|
@@ -198,6 +203,25 @@ DUPLICATE_CONTENT_FILE_REMEDY: Final = (
|
|
|
198
203
|
)
|
|
199
204
|
|
|
200
205
|
|
|
206
|
+
#: Cure for `AliasItemCollisionError` (SEC-13, T-21). An alias key and an item id
|
|
207
|
+
#: must not be the same string, or a lookup for the retired id resolves to the
|
|
208
|
+
#: item the alias points at and surfaces content the retired item withholds. The
|
|
209
|
+
#: one shape this allows is a rename: deprecate the old item first, then alias it.
|
|
210
|
+
#: Names the rebuild path as the second half of one remedy for the same reason
|
|
211
|
+
#: `DUPLICATE_CONTENT_FILE_REMEDY` does -- editing an applied migration trips
|
|
212
|
+
#: FR-K5's checksum guard, so a remedy that stopped at "edit it" would loop the
|
|
213
|
+
#: reader between the two (issue #63's HIGH-1).
|
|
214
|
+
ALIAS_ITEM_COLLISION_REMEDY: Final = (
|
|
215
|
+
"Remove the addAlias, or give the item a distinct id -- an alias key and an item id must "
|
|
216
|
+
"not be the same string. If this is a rename, deprecate the old item first "
|
|
217
|
+
"(deprecateItem), the one shape this allows: the retired id then resolves to its "
|
|
218
|
+
"successor without exposing withheld content. If the offending migration was already "
|
|
219
|
+
"applied, editing it also trips the applied-migration checksum guard -- delete "
|
|
220
|
+
"`.theurian/state/` after the edit and run `theurian migrate apply`, which rebuilds "
|
|
221
|
+
"canonical state from the corrected migrations (FR-K4)."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
201
225
|
#: Every canonical-state database this project has ever built. Excludes
|
|
202
226
|
#: `theurian-index-*.sqlite`, which lives in the same directory
|
|
203
227
|
#: (`ProjectPaths.state`) but is a different schema entirely.
|
|
@@ -334,20 +358,41 @@ def _refuse_a_body_file_backing_two_revisions(
|
|
|
334
358
|
)
|
|
335
359
|
|
|
336
360
|
|
|
361
|
+
def _refuse_an_alias_colliding_with_an_item(migration_set: MigrationSet, *, as_json: bool) -> None:
|
|
362
|
+
"""Report T-21's whole-set refusal identically at `migrate validate` and `apply`.
|
|
363
|
+
|
|
364
|
+
One function rather than a `try`/`except` in each command, so the two cannot
|
|
365
|
+
drift on a statically decidable rule (issue #36's class) -- and so `apply`
|
|
366
|
+
refuses before `create_database` runs and leaves no state database behind, the
|
|
367
|
+
way the scope and body-file guards already do.
|
|
368
|
+
"""
|
|
369
|
+
try:
|
|
370
|
+
refuse_alias_item_id_collision(migration_set)
|
|
371
|
+
except AliasItemCollisionError as exc:
|
|
372
|
+
_fail(
|
|
373
|
+
str(exc),
|
|
374
|
+
remedy=ALIAS_ITEM_COLLISION_REMEDY,
|
|
375
|
+
as_json=as_json,
|
|
376
|
+
code=EXIT_STATE_ERROR,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
337
380
|
def _refused_migration_ids(migration_set: MigrationSet) -> list[str]:
|
|
338
381
|
"""Every migration id `migrate validate`/`apply` would refuse, for `status`.
|
|
339
382
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
`refusedIds: []` for a set `validate`/`apply`
|
|
345
|
-
first-seen order (a migration can carry
|
|
383
|
+
Three gating rules feed `refusedIds`: the tenant/ACL scope rule (issue #63),
|
|
384
|
+
the one-body-one-revision rule (issue #210), and the alias/item-id collision
|
|
385
|
+
rule (SEC-13, T-21). Each has a non-throwing enumerator split from its
|
|
386
|
+
throwing refusal precisely so `status` can report without gating -- reporting
|
|
387
|
+
only some of them told a reader `refusedIds: []` for a set `validate`/`apply`
|
|
388
|
+
exit 4 on. Deduplicated in first-seen order (a migration can carry more than
|
|
389
|
+
one fault), so the field is stable.
|
|
346
390
|
"""
|
|
347
391
|
seen: dict[str, None] = {}
|
|
348
392
|
for migration_id in (
|
|
349
393
|
*unenforceable_scope_violations(migration_set),
|
|
350
394
|
*duplicate_content_file_violations(migration_set),
|
|
395
|
+
*alias_item_collision_violations(migration_set),
|
|
351
396
|
):
|
|
352
397
|
seen.setdefault(str(migration_id), None)
|
|
353
398
|
return list(seen)
|
|
@@ -1112,6 +1157,7 @@ def migrate_validate(as_json: JsonOption = False) -> None:
|
|
|
1112
1157
|
return
|
|
1113
1158
|
|
|
1114
1159
|
_refuse_a_body_file_backing_two_revisions(context.loaded.migration_set, as_json=as_json)
|
|
1160
|
+
_refuse_an_alias_colliding_with_an_item(context.loaded.migration_set, as_json=as_json)
|
|
1115
1161
|
|
|
1116
1162
|
_emit(
|
|
1117
1163
|
{
|
|
@@ -1150,11 +1196,12 @@ def migrate_apply(as_json: JsonOption = False) -> None:
|
|
|
1150
1196
|
)
|
|
1151
1197
|
return
|
|
1152
1198
|
|
|
1153
|
-
# `MigrationEngine.apply` refuses
|
|
1199
|
+
# `MigrationEngine.apply` refuses these too, but only once a write
|
|
1154
1200
|
# transaction is open and `create_database` has already run. Refusing here
|
|
1155
1201
|
# as well is what keeps issue #63's property -- a refused apply leaves no
|
|
1156
|
-
# database file behind -- true for
|
|
1202
|
+
# database file behind -- true for these rules too (issue #210, T-21).
|
|
1157
1203
|
_refuse_a_body_file_backing_two_revisions(context.loaded.migration_set, as_json=as_json)
|
|
1204
|
+
_refuse_an_alias_colliding_with_an_item(context.loaded.migration_set, as_json=as_json)
|
|
1158
1205
|
|
|
1159
1206
|
# This installation's record of the state it built, out of the repository
|
|
1160
1207
|
# tree (ADR-0004, SEC-7). Used twice below: to refuse to *apply into* a
|