theurian 0.1.0.dev2__tar.gz → 0.1.0.dev3__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.dev2 → theurian-0.1.0.dev3}/.gitignore +16 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/CHANGELOG.md +492 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/PKG-INFO +1 -1
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/pyproject.toml +1 -1
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/__init__.py +1 -1
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/project_service.py +86 -10
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/setup_service.py +52 -5
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/setup_steps.py +146 -17
- theurian-0.1.0.dev3/src/theurian/cli/auth_commands.py +173 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/cli/commands.py +17 -1
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/cli/setup_commands.py +19 -4
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/identifiers.py +13 -9
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/canonical_store.py +62 -1
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/setup.py +11 -3
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/state.py +24 -1
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/secrets/file_store.py +6 -2
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/schema.py +8 -3
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/store.py +193 -31
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/mcp/search.py +27 -4
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/mcp/tools.py +206 -18
- theurian-0.1.0.dev3/src/theurian/security/env_file.py +419 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/fakes/store.py +42 -1
- theurian-0.1.0.dev3/tests/integration/test_auth_rotate.py +258 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_canonical_store.py +198 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_canonical_store_corruption.py +256 -21
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_daemon.py +1 -1
- theurian-0.1.0.dev3/tests/integration/test_init_gitignore_block.py +297 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_mcp_tools.py +1262 -20
- theurian-0.1.0.dev3/tests/integration/test_revision_id_reuse.py +404 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_setup_cli.py +158 -1
- theurian-0.1.0.dev3/tests/integration/test_setup_env_file.py +816 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_setup_journal.py +5 -3
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_setup_partial_failure.py +16 -6
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_setup_report_withholding.py +12 -1
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_setup_service.py +54 -3
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_wire_contract.py +168 -2
- theurian-0.1.0.dev3/tests/integration/test_writer_contract.py +231 -0
- theurian-0.1.0.dev3/tests/unit/test_env_file_merge.py +799 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_migration_engine.py +56 -0
- theurian-0.1.0.dev3/theurian/schemas/mcp/knowledge-search-response.schema.json +55 -0
- theurian-0.1.0.dev3/theurian/schemas/mcp/knowledge-status-response.schema.json +83 -0
- theurian-0.1.0.dev2/src/theurian/cli/auth_commands.py +0 -103
- theurian-0.1.0.dev2/src/theurian/security/env_file.py +0 -31
- theurian-0.1.0.dev2/tests/integration/test_auth_rotate.py +0 -98
- theurian-0.1.0.dev2/theurian/schemas/mcp/knowledge-search-response.schema.json +0 -38
- theurian-0.1.0.dev2/theurian/schemas/mcp/knowledge-status-response.schema.json +0 -66
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/LICENSE +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/README.md +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/hatch_build.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/forest_builder.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/index_builder.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/ingestion_service.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/migration_engine.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/retrieval_service.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/setup_context.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/setup_withholding.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/visibility.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/application/withdrawal_purge.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/cli/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/cli/context.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/cli/index_commands.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/cli/main.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/daemon/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/daemon/instance.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/daemon/runner.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/daemon/server.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/chunking.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/compatibility.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/context.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/enums.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/errors.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/extras.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ingestion.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/knowledge.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/migration.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/authorization.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/daemon_manager.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/determinism.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/embedding.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/index_store.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/mcp_client_config.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/object_store.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/reranking.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/review_provider.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/secret_store.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/source_parser.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/specification_provider.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/summarization.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ports/vector_store.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/project.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/ranking.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/raptor.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/retrieval.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/review.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/specification.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/domain/values.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/indexing/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/claude/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/claude/mcp_config.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/determinism.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/embedding/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/embedding/hashing.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/filesystem/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/filesystem/migration_loader.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/filesystem/parsers/markdown.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/filesystem/parsers/openapi.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/filesystem/parsers/registry.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/filesystem/parsers/structured.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/git/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/github/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/raptor/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/raptor/extractive.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/secrets/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/services/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/services/launchagent.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/services/runner.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/services/systemd_user.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/connection.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/index_forest.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/index_purge.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/index_query.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/index_scan.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/index_schema.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/sqlite/index_store.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/infrastructure/vector/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/ingestion/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/mcp/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/mcp/results.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/migrations/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/normalization/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/normalization/projection.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/observability/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/retrieval/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/review/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/security/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/security/paths.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/security/tokens.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/security/yaml_loading.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/specification/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/src/theurian/traceability/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/conftest.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/fakes/__init__.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/fakes/clock.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/fakes/ids.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/fakes/pages.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/fakes/setup.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_absence_proof.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_bare_install.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_claude_mcp_config.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_cli_commands.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_cli_help_without_rich.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_forest_builder.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_forest_builder_scale.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_forest_node_scope.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_forest_purge_equality.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_forest_purge_recompute.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_forest_retrieval.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_forest_store_retrieval.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_gc_during_a_search.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_fallback.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_gc_cli.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_purge.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_purge_differential.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_purge_nodes.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_scan_fold.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_schema_v4.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_index_store.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_ingestion.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_project_registry.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_retrieval_service.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_scan_exhaustion.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_service_adapters.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_session_start_hook.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_setup_probe_assertions.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_short_query_retrieval.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_unreadable_registry_surface.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/integration/test_withdrawal_purge.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_artifact_integrity_claim.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_candidate_cut.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_candidate_depth.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_chunking.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_cli_help_rendering.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_compatibility.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_daemon_extra.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_domain_invariants.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_examples.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_extractive_summarizer.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_forest_derivation.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_forest_fanout.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_gate_call_sites.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_hashing_embedding.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_install_claims.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_layering.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_parsers.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_path_security.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_plugin_boundary.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_ports.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_project_and_traceability.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_projection.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_ranking.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_raptor_scope.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_result_gate_session.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_result_payload.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_retrieval_depth.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_schemas.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_scope_isolation.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_setup_changed_paths.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_setup_claims.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_setup_domain.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_state_hash.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_test_fixtures.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_tokens.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/tests/unit/test_yaml_loading.py +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/README.md +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/cli/version.schema.json +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/config/project-config.schema.json +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/knowledge/retrieval-result.schema.json +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/mcp/project-list-response.schema.json +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/mcp/retrieval-metadata.schema.json +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/mcp/tool-context.schema.json +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/migrations/migration.schema.json +0 -0
- {theurian-0.1.0.dev2 → theurian-0.1.0.dev3}/theurian/schemas/protocol/compatibility.schema.json +0 -0
|
@@ -30,6 +30,22 @@ htmlcov/
|
|
|
30
30
|
*.sqlite-wal
|
|
31
31
|
*.sqlite-shm
|
|
32
32
|
|
|
33
|
+
# --- Documentation site (derived) ---
|
|
34
|
+
# `mkdocs build` writes site/. The Pages workflow copies the logo into
|
|
35
|
+
# docs/assets/ before building, so anyone replicating that build locally
|
|
36
|
+
# creates it too; the tracked copy is assets/theurian-logo.svg.
|
|
37
|
+
#
|
|
38
|
+
# The workflow also fetches the mermaid bundle into docs/js/ before building.
|
|
39
|
+
#
|
|
40
|
+
# Every pattern in this section is anchored, unlike the older ones above, and
|
|
41
|
+
# the anchors matter here. An unanchored site/ would also ignore docs/site/,
|
|
42
|
+
# packages/*/site/ and tests/fixtures/site/. Naming the generated files rather
|
|
43
|
+
# than the directories docs/assets/ and docs/js/ leaves anything else added to
|
|
44
|
+
# them visible to git status — docs/js/ also holds tracked source.
|
|
45
|
+
/site/
|
|
46
|
+
/docs/assets/theurian-logo.svg
|
|
47
|
+
/docs/js/mermaid.min.js
|
|
48
|
+
|
|
33
49
|
# --- OS / editor ---
|
|
34
50
|
.DS_Store
|
|
35
51
|
.idea/
|
|
@@ -12,6 +12,498 @@ 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.dev3] - 2026-08-15
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **A present-only `integrity` object discloses derived-state damage on
|
|
20
|
+
`knowledge.search`, `knowledge.get` and `knowledge.status`**
|
|
21
|
+
([#30](https://github.com/theurian/theurian/issues/30), PR1 of five positions —
|
|
22
|
+
one closes here, four remain).
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"integrity": {
|
|
27
|
+
"damageDetected": true,
|
|
28
|
+
"remedy": "Run `theurian migrate apply` to rebuild the derived state from the Git-tracked migrations. If this signal persists, delete `.theurian/state/` and run `theurian migrate apply` again, then `theurian index build` to restore ranked retrieval; the state is derived, so nothing is lost."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**The key is present only when a bounded check detected a discrepancy, and its
|
|
34
|
+
absence asserts nothing.** No `integrity` key means the check did not fire —
|
|
35
|
+
which is *not* "verified clean" and must not be read as one. There is
|
|
36
|
+
deliberately no `damageDetected: false` form: the detector is incomplete by
|
|
37
|
+
design, so a `false` token would publish "checked and clean" over a check that
|
|
38
|
+
never made that claim, while absence cannot be misread without a caller
|
|
39
|
+
inventing a claim of its own. `damageDetected` is therefore always `true` when
|
|
40
|
+
present, kept explicit rather than reduced to a bare boolean so the object can
|
|
41
|
+
gain a second field without a wire break. This is the same present-only shape
|
|
42
|
+
`raptorPath` already uses (ADR-0008 decision 8), so the wire already branches on
|
|
43
|
+
key presence; the schema declares the key as an optional property precisely so
|
|
44
|
+
`additionalProperties: false` keeps holding when it appears
|
|
45
|
+
(`knowledge-search-response.schema.json`,
|
|
46
|
+
`knowledge-status-response.schema.json`; `knowledge.get` still publishes no
|
|
47
|
+
response schema, [#20](https://github.com/theurian/theurian/issues/20)).
|
|
48
|
+
|
|
49
|
+
**What PR1 detects is a migration-count mismatch, and nothing finer.**
|
|
50
|
+
`expected` is the active pointer's own `migrationCount`, carried from the same
|
|
51
|
+
resolution of `active.json` that chose the state database rather than re-read;
|
|
52
|
+
`live` is `SELECT COUNT(*) FROM migration_history INDEXED BY
|
|
53
|
+
idx_migration_history_sequence WHERE project_id = ?`. The state database is
|
|
54
|
+
immutable once built, so a healthy project has `live == expected` and any
|
|
55
|
+
difference is damage — `!=`, not `<`, so another project's rows reaching this
|
|
56
|
+
one count too (`test_a_surplus_migration_row_is_damage_on_every_read_tool`,
|
|
57
|
+
which is RED against `>=` in place of `!=`; the `WHERE project_id = ?` that
|
|
58
|
+
keeps a *sibling* project's rows out of `live` is
|
|
59
|
+
`test_a_sibling_projects_rows_in_the_same_file_forge_no_mismatch`). Both sides
|
|
60
|
+
are pinned: a lost row surfaces the field from each of the three tools
|
|
61
|
+
(`test_a_lost_migration_row_surfaces_integrity_from_knowledge_search`,
|
|
62
|
+
`…_status`, `…_get`, each RED when that tool's emission is unplugged) and a
|
|
63
|
+
healthy build emits it from none of them
|
|
64
|
+
(`test_a_healthy_build_emits_no_integrity_field_from_any_tool`, and
|
|
65
|
+
`test_a_re_apply_and_a_third_migration_leave_every_tool_silent` for the same
|
|
66
|
+
silence after the pointer has moved). The wire form is validated against the
|
|
67
|
+
schemas from a damaged project rather than a healthy one, where the optional
|
|
68
|
+
key is never present to check
|
|
69
|
+
(`test_the_damaged_captures_really_carry_the_optional_integrity_key`,
|
|
70
|
+
`test_the_integrity_conformance_check_can_fail`).
|
|
71
|
+
|
|
72
|
+
**The signal carries no bit about withheld content, and its cost carries none
|
|
73
|
+
either.** It reads `migration_history`, a table no gate filters, so nothing it
|
|
74
|
+
counts scales with the withheld set —
|
|
75
|
+
`test_the_integrity_signal_is_identical_across_a_withheld_only_difference`
|
|
76
|
+
measures whether the key appears across two corpora differing only in
|
|
77
|
+
twenty-five `rejected` items and asserts it is identical for all three tools.
|
|
78
|
+
The added per-request read on `knowledge.search` is answered from the covering
|
|
79
|
+
index — SQLite plans `SEARCH migration_history USING COVERING INDEX
|
|
80
|
+
idx_migration_history_sequence (project_id=?)`, so its cost is `O(migrations)`
|
|
81
|
+
and independent of the corpus — which is what keeps it off the `O(withheld)`
|
|
82
|
+
timing channels [#19](https://github.com/theurian/theurian/issues/19) and
|
|
83
|
+
[#158](https://github.com/theurian/theurian/issues/158) closed
|
|
84
|
+
(`test_the_search_integrity_count_is_answered_by_a_covering_index`, pinning both
|
|
85
|
+
the `INDEXED BY` hint in the statement the store really runs and the plan
|
|
86
|
+
SQLite produces for it). The plan assertion requires the *seek* — `SEARCH`, the
|
|
87
|
+
index name, and the `(project_id=?` that opens the constraint list — because the
|
|
88
|
+
index name alone appears on a `SCAN` line too: reversing the declared columns to
|
|
89
|
+
`(sequence, project_id)` keeps the name and walks every project's migration
|
|
90
|
+
entries at 172× the work, measured, and passed the earlier substring. The same
|
|
91
|
+
strengthening was applied to `idx_items_status`'s assertion in
|
|
92
|
+
`test_status_count_is_answered_by_a_covering_index`.
|
|
93
|
+
|
|
94
|
+
**`knowledge.get` distinguishes damage from absence in its refusal message.**
|
|
95
|
+
It refuses with a bare string and no field, so the distinction lives in the
|
|
96
|
+
text: where the check reports damage, an item it could not read is now reported
|
|
97
|
+
as a project that "could not be fully read: its derived state holds a different
|
|
98
|
+
number of migration-history rows than its own records expect", not as an item
|
|
99
|
+
that is not present. Both directions are pinned, because either alone is
|
|
100
|
+
satisfied by a tool that says one thing always
|
|
101
|
+
(`test_an_absent_item_over_a_damaged_state_is_refused_as_damage_not_absence`,
|
|
102
|
+
`test_an_absent_item_over_a_healthy_state_is_refused_as_absence`). The SEC-13
|
|
103
|
+
rule that a withheld id and an absent id get the same message is unchanged.
|
|
104
|
+
|
|
105
|
+
**What this does not cover.** Four `SILENTLY_EMPTIED` positions remain and are
|
|
106
|
+
carried to PR2 — `(knowledge.search, knowledge_items, item_id)`,
|
|
107
|
+
`(knowledge.search, knowledge_items, project_id)`,
|
|
108
|
+
`(knowledge.status, knowledge_items, project_id)`,
|
|
109
|
+
`(knowledge.status, knowledge_items, status)`. A migration-count check cannot
|
|
110
|
+
see any of them: they empty a result rather than the migration history, so
|
|
111
|
+
`live` still equals `expected` and the key stays absent exactly as on a healthy
|
|
112
|
+
project. That is what "absence asserts nothing" means in practice.
|
|
113
|
+
|
|
114
|
+
**The `remedy` names a fallback, because one command does not cure both
|
|
115
|
+
directions.**
|
|
116
|
+
`theurian migrate apply` is the cheap cure and comes first — measured, it clears
|
|
117
|
+
a lost row, a sentinel in `migration_history.project_id`, and an over-counting
|
|
118
|
+
pointer. It clears nothing for a *surplus* row: every authored migration is
|
|
119
|
+
already applied, so three consecutive runs exited 0 with `applied: [], changed:
|
|
120
|
+
false` and left the key present. Deleting `.theurian/state/` makes the next apply
|
|
121
|
+
rebuild the database (`databaseCreated: true`, key absent), and `theurian index
|
|
122
|
+
build` is named third because that deletion takes the published retrieval index
|
|
123
|
+
with it — measured, `retrieval.indexed` is `false` with `fallbackReason:
|
|
124
|
+
"no-index"` until the rebuild runs, so without the third step "nothing is lost"
|
|
125
|
+
would be false. The efficacy is measured, not yet pinned by a test.
|
|
126
|
+
|
|
127
|
+
Two further limits, measured rather than assumed, both recorded in
|
|
128
|
+
[the threat model](../../docs/security/threat-model.md) under T-17:
|
|
129
|
+
|
|
130
|
+
- **A pointer whose `migrationCount` is wrong in the same direction as the rows
|
|
131
|
+
is undetectable.** The check compares two derived numbers against each other,
|
|
132
|
+
never against the Git-tracked migrations, so a state that lost its migration
|
|
133
|
+
row *and* a pointer recording `0` agree — measured, all three tools answer,
|
|
134
|
+
`knowledge.status` publishes `appliedMigrations: 0` against a project holding
|
|
135
|
+
one applied migration, and `migrate status`, `migrate apply` and `index build`
|
|
136
|
+
all exit 0. A pointer wrong on its own does fire the key (measured at `2` and
|
|
137
|
+
at `0` against one live row), but the signal cannot say which side is wrong,
|
|
138
|
+
and `appliedMigrations` publishes the pointer's number either way.
|
|
139
|
+
- **Corrupt `migration_history.applied_at` or `.sequence` is seen by no shipped
|
|
140
|
+
surface at all** — measured: all three tools answer cleanly, and `migrate
|
|
141
|
+
status`, `migrate apply` (with and without a new migration to apply) and
|
|
142
|
+
`index build` all exit 0.
|
|
143
|
+
|
|
144
|
+
### Changed
|
|
145
|
+
|
|
146
|
+
- **`knowledge.status` reports `appliedMigrations` from the active pointer's
|
|
147
|
+
`migrationCount`, not from a live count of `migration_history` rows**
|
|
148
|
+
([#30](https://github.com/theurian/theurian/issues/30) PR1). On a healthy
|
|
149
|
+
project the two are equal by construction and no response changes. They diverge
|
|
150
|
+
only under damage, and there the pointer's count is the authoritative one:
|
|
151
|
+
before this change a corrupt `migration_history.project_id` dropped every row
|
|
152
|
+
out of the `WHERE`, so the tool answered `appliedMigrations: 0` against a
|
|
153
|
+
project that had applied several — a successful, false statement, and the
|
|
154
|
+
`SILENTLY_EMPTIED` position PR1 closes. The live count is now compared against
|
|
155
|
+
the pointer and any difference — in either direction — disclosed through
|
|
156
|
+
`integrity` instead of published as the answer
|
|
157
|
+
(`test_a_corrupt_migration_project_id_is_disclosed_not_silently_emptied`;
|
|
158
|
+
`test_no_tool_answers_with_less_than_the_intact_database_holds` holds the set at
|
|
159
|
+
four members and goes RED if the field starts shrinking again). **A behaviour
|
|
160
|
+
change for a caller that compares this field against a row count it obtained
|
|
161
|
+
some other way**: over a damaged state the two now disagree by design, and the
|
|
162
|
+
`integrity` key is what says so.
|
|
163
|
+
|
|
164
|
+
- **A negative `migrationCount` in `.theurian/state/active.json` is now refused
|
|
165
|
+
at parse time, where it used to be published**
|
|
166
|
+
([#30](https://github.com/theurian/theurian/issues/30) PR1). `knowledge.status`
|
|
167
|
+
reports that field as `appliedMigrations`, whose schema declares `minimum: 0`,
|
|
168
|
+
and `ActiveState.from_json` accepted any integer. Measured before the fix:
|
|
169
|
+
`migrationCount: -5` reached the wire as `appliedMigrations: -5`, so the
|
|
170
|
+
response violated its own published contract — and a strict client rejects the
|
|
171
|
+
whole response, including the `integrity` key riding along on it to say the
|
|
172
|
+
state is damaged. The one field reporting the damage was thrown away by the
|
|
173
|
+
damage. It is now a `DomainError` at parse time, converted by
|
|
174
|
+
`read_active_state` into the `ProjectError` a corrupt pointer already produced,
|
|
175
|
+
so all three read tools refuse with "Malformed active state pointer:
|
|
176
|
+
migrationCount is negative (-5)" and the delete-the-pointer-and-re-apply cure
|
|
177
|
+
(`test_a_negative_migration_count_is_refused_by_every_read_tool`). **A behaviour
|
|
178
|
+
change for anyone who hand-edits the pointer**: a value that used to be answered
|
|
179
|
+
with is now a refusal. Only negative values are refused — a non-negative integer
|
|
180
|
+
that is simply wrong is still accepted, which is the one-way limit recorded
|
|
181
|
+
above and in the threat model.
|
|
182
|
+
([#30](https://github.com/theurian/theurian/issues/30) PR1). The refusal was a
|
|
183
|
+
side effect of parsing rows the tool no longer reads: it used to call
|
|
184
|
+
`applied_migrations`, which converts both cells and raises on a damaged one,
|
|
185
|
+
and it now calls a bare `COUNT` that interprets neither. Measured on the
|
|
186
|
+
corruption corpus: with a sentinel in either column the tool returns its six
|
|
187
|
+
keys and no `integrity` — the count is unaffected, so the check does not fire —
|
|
188
|
+
while `applied_migrations` over the same database still raises
|
|
189
|
+
`StateDatabaseUnreadableError`. No published `knowledge.status` field is
|
|
190
|
+
derived from either cell, and `migrate status` and `migrate apply` still exit 4
|
|
191
|
+
over both, so migration tamper is still detected where it is acted on. Recorded
|
|
192
|
+
rather than marked BREAKING because no published contract promised the refusal
|
|
193
|
+
and no field, type or tool name changed (see *Changing this contract* in
|
|
194
|
+
[`docs/protocol/mcp-tools.md`](../../docs/protocol/mcp-tools.md)).
|
|
195
|
+
|
|
196
|
+
**It is a real reduction in what the read tools notice, and it is now pinned as
|
|
197
|
+
an exact six-cell set** rather than left to a reader:
|
|
198
|
+
`ANSWERED_CLEAN_OVER_A_DAMAGED_CELL` in
|
|
199
|
+
`tests/integration/test_canonical_store_corruption.py` names all three tools
|
|
200
|
+
over both cells, and
|
|
201
|
+
`test_exactly_these_positions_answer_cleanly_over_a_cell_the_cli_calls_tampering`
|
|
202
|
+
reads it against the CLI sweep — so the silence is only green while `migrate
|
|
203
|
+
status` and `migrate apply` keep exiting non-zero on the same cells, and a read
|
|
204
|
+
tool that starts refusing again fails it too. Its counterpart
|
|
205
|
+
`test_exactly_these_positions_disclose_migration_history_damage_as_integrity`
|
|
206
|
+
keeps that set from going vacuous by naming the three positions
|
|
207
|
+
(`migration_history.project_id` on each tool) that must fire the key.
|
|
208
|
+
|
|
209
|
+
- **`theurian setup` and `theurian auth rotate` rewrite only the Theurian-owned
|
|
210
|
+
block in `<data_dir>/env`, and leave every other byte of that file alone**
|
|
211
|
+
([#128](https://github.com/theurian/theurian/issues/128)). Both used to render
|
|
212
|
+
the whole file and truncate whatever else was in it, and `probe_env_reference`
|
|
213
|
+
reported `Missing` on any difference — so a line somebody had added to a file
|
|
214
|
+
whose own header says "Sourced by your shell profile" was destroyed with no
|
|
215
|
+
diff, no backup and no mention in `changedPaths`, on every run of a command whose
|
|
216
|
+
contract is that running it twice changes nothing (FR-L2). §6.2 row 7 of the
|
|
217
|
+
requirements analysis had required "rewrite the Theurian-owned block only"
|
|
218
|
+
throughout. The block is delimited by `# >>> theurian >>>` and
|
|
219
|
+
`# <<< theurian <<<`, spelled exactly as the pair `theurian init` writes into a
|
|
220
|
+
`.gitignore` so that someone who has seen one managed block recognises the
|
|
221
|
+
other, and the merge is computed *before* the file is opened — so a file that
|
|
222
|
+
cannot be delimited is never opened at all.
|
|
223
|
+
|
|
224
|
+
**What an operator upgrading from `0.1.0.dev0`–`dev2` sees.** Those versions
|
|
225
|
+
wrote the whole file as a fixed rendering of the data directory, so the first
|
|
226
|
+
`setup` or `auth rotate` after upgrading recognises that rendering and replaces
|
|
227
|
+
it *in place* with the marked block: one `export THEURIAN_MCP_TOKEN` afterwards
|
|
228
|
+
and not two, with lines added before it still before it and lines added after
|
|
229
|
+
it still after it. Appending the block beside the old rendering would have left
|
|
230
|
+
two assignments naming different paths once a data directory moves — the shell
|
|
231
|
+
taking whichever came last, while setup reported the machine converged.
|
|
232
|
+
|
|
233
|
+
**Recognition is exact, and deliberately not fuzzy**: those lines must be
|
|
234
|
+
consecutive and whole, and must name *this* data directory's token path. A
|
|
235
|
+
rendering somebody edited, and one written for another installation, are
|
|
236
|
+
therefore left alone and the block is appended below them — two visible
|
|
237
|
+
exports, the shell keeping the block because it reads it last. That is the
|
|
238
|
+
honest answer for a line somebody changed on purpose; matching it loosely is
|
|
239
|
+
what glued the block over half of one in the first cut.
|
|
240
|
+
|
|
241
|
+
**A marker is a whole line, and the first cut of this fix did not do that.**
|
|
242
|
+
Review found it substring-based: `str.find` opened the span at the first
|
|
243
|
+
*occurrence* of the start marker, so `echo "everything between
|
|
244
|
+
# >>> theurian >>> and here"` opened one and the rewrite cut that line in half,
|
|
245
|
+
leaving an unclosed quote that poisons every line after it in a sourced file;
|
|
246
|
+
the count that was supposed to catch a second start looked only at what
|
|
247
|
+
followed the *end* marker, so `S`, a user's line, `S`, the block, `E` — what
|
|
248
|
+
repairing an unterminated block by pasting a fresh one under it leaves — was
|
|
249
|
+
swallowed whole; and the dev0–dev2 rendering was matched as a substring, so
|
|
250
|
+
`export THEURIAN_MCP_TOKEN # my note` had the block spliced over its first
|
|
251
|
+
half and the leftovers glued onto the end marker. Measured over every file a
|
|
252
|
+
start marker, an end marker and a user's line build up to five lines long, 363
|
|
253
|
+
arrangements: **39 took the wrong refusal decision and 16 of those reported
|
|
254
|
+
success while dropping 19 of the user's lines**, one of them an
|
|
255
|
+
`export AWS_SECRET_ACCESS_KEY`, with the run reporting `converged` and the
|
|
256
|
+
re-probe `satisfied`. What shipped matches whole lines — split on `\n` alone,
|
|
257
|
+
a trailing `\r` dropped, so a CRLF file delimits — and counts the start lines
|
|
258
|
+
over the whole file *before* choosing a span, which is what makes the second
|
|
259
|
+
one's position irrelevant.
|
|
260
|
+
|
|
261
|
+
**A behaviour change for a file whose markers do not delimit exactly one
|
|
262
|
+
block** — two or more start lines anywhere in the file, or a start line with no
|
|
263
|
+
end line after it. That used to be overwritten; it is now `Conflicting`,
|
|
264
|
+
because once the delimiters disagree setup cannot tell which lines are its own.
|
|
265
|
+
An *end* line with no start above it, and a second end line, are not that: they
|
|
266
|
+
delimit nothing, and they are kept like any other line. `setup` writes nothing
|
|
267
|
+
there, declares no path for it, and stops at consent; `--approve-conflicts`
|
|
268
|
+
applies the rest of the plan and still leaves that file byte-identical,
|
|
269
|
+
finishing `degraded` with the step named in `warnings`. `auth rotate` leaves the
|
|
270
|
+
file untouched, **still rotates the token**, and prepends one line to
|
|
271
|
+
`nextSteps` naming the file to repair — an exposed credential outranks a comment
|
|
272
|
+
marker, and the token has already been replaced by the time that file is
|
|
273
|
+
reached. The same holds when the OS refuses the write — a read-only checkout, a
|
|
274
|
+
file another account owns, a full disk: rotation completes and `nextSteps`
|
|
275
|
+
carries the exception's class name and never its message, which holds
|
|
276
|
+
`strerror`, the errno and on some platforms a second path. The conflict detail
|
|
277
|
+
carries the two marker strings, the path they are in and the command to re-run,
|
|
278
|
+
and no other line out of that file, because `doctor --report` publishes it
|
|
279
|
+
(O-3, SEC-6).
|
|
280
|
+
|
|
281
|
+
**A run can be right about the block and wrong about the machine, and now says
|
|
282
|
+
so for the direct assignment forms.** A shell keeps the last assignment it
|
|
283
|
+
reads, so a line *below* the block assigning `THEURIAN_MCP_TOKEN` again is what
|
|
284
|
+
gets exported while the probe — deliberately blind to lines it does not own —
|
|
285
|
+
reports the block current. That line is not Theurian's to edit and it is not a
|
|
286
|
+
conflict either; the step stays `satisfied` and carries a caveat, which
|
|
287
|
+
`_reservations` turns into a warning, so the run ends **`degraded` where it
|
|
288
|
+
used to end `converged`**. The warning names the path, the variable and the
|
|
289
|
+
start marker to move the line above, and never the line itself. A bare
|
|
290
|
+
`export THEURIAN_MCP_TOKEN` or a commented-out assignment is not an override
|
|
291
|
+
and leaves the run converged. Currency is asked first, so a block that is both
|
|
292
|
+
stale and shadowed is rewritten and *then* reported, rather than reported
|
|
293
|
+
instead of fixed.
|
|
294
|
+
|
|
295
|
+
**What finds that line is a heuristic, and the scope is published rather than
|
|
296
|
+
implied.** `contains_shadowing_assignment` reads one line at a time and
|
|
297
|
+
recognises a first word spelled `THEURIAN_MCP_TOKEN=…`, or that word after
|
|
298
|
+
`export`, `declare`, `typeset` or `readonly`. It is wrong in both directions,
|
|
299
|
+
measured with `/bin/bash` sourcing the block and then the line: an `&&` list,
|
|
300
|
+
an `if`/`then`, a `{ }` group and an `eval` each assign the variable while the
|
|
301
|
+
run stays **silent and `converged`**, and an assignment inside a quoted heredoc
|
|
302
|
+
*body* draws the warning although the shell keeps the block's value. The four
|
|
303
|
+
misses are pinned **as** the recorded boundary, each through a real shell
|
|
304
|
+
rather than restated against the function, so a change that starts warning on
|
|
305
|
+
one of them has to arrive with an argument and update the pin deliberately. Not
|
|
306
|
+
extended, and that is the decision rather than a to-do: what a line does is
|
|
307
|
+
settled by the shell at run time — `eval` takes a string that need not exist
|
|
308
|
+
until then, and a heredoc body is not shell at all — and a probe that runs
|
|
309
|
+
somebody's shell profile is not a probe. The residual is carried in the wording
|
|
310
|
+
instead: both published sentences say the line *appears* to assign and the
|
|
311
|
+
block *appears* to be overridden, which is what keeps the heredoc case honest,
|
|
312
|
+
and both are pinned — dropping the hedge from the `summary` alone, the sentence
|
|
313
|
+
a reader who stops at `satisfied` sees, survived all 2,442 tests while the
|
|
314
|
+
`detail`'s was held. **What stays unqualified** is the other arm's summary,
|
|
315
|
+
"`…/env` exports `THEURIAN_MCP_TOKEN` by reference" — and the `converged` the
|
|
316
|
+
run reports beside it. On a machine using one of the four evading shapes both
|
|
317
|
+
are true of the block and incomplete about the machine; measured through the
|
|
318
|
+
real CLI, `theurian doctor --json` publishes that summary, zero warnings and
|
|
319
|
+
exit 0 while `bash` exports the later line's value. Recorded here and in §6.2
|
|
320
|
+
row 7 rather than fixed, because no line-level rule can tell that machine from
|
|
321
|
+
a healthy one.
|
|
322
|
+
|
|
323
|
+
**`theurian doctor` and `theurian setup --dry-run` now carry that warning
|
|
324
|
+
too**, which is the caller-visible half of this. The sentence was built in the
|
|
325
|
+
verification pass alone, so on one machine `theurian setup` said `degraded`
|
|
326
|
+
with the caveat while `theurian doctor --json` said `"warnings": []` and exited
|
|
327
|
+
0 — the caveat sitting in the payload the whole time as the `detail` of a step
|
|
328
|
+
whose status reads `satisfied`, which is where a reader stops. Both surfaces
|
|
329
|
+
that publish a plan now build their warnings with the same `_reservations`, so
|
|
330
|
+
a shadowed machine gains one `env-reference: …` line in `doctor --json`,
|
|
331
|
+
`doctor --report` and the `--dry-run` plan the plugin renders. Nothing else
|
|
332
|
+
moved, deliberately: a reservation is a finding with no work attached, so
|
|
333
|
+
`healthy`, `problemCount` and the exit status stay tied to what setup would
|
|
334
|
+
change and what needs consent, and a machine whose only finding is a line
|
|
335
|
+
Theurian will not touch still exits 0. The reports built on the way *past* a
|
|
336
|
+
plan — `aborted`, `awaiting_consent` and `halted` — carry no reservations, and
|
|
337
|
+
that is recorded rather than closed in `_reservations`' docstring: each hands
|
|
338
|
+
the reader a larger question first, and the step's `detail` still travels with
|
|
339
|
+
the report.
|
|
340
|
+
|
|
341
|
+
**Line endings are bytes somebody chose.** Both writers read and write with
|
|
342
|
+
newline translation off, so a file edited on Windows keeps its `\r` bytes
|
|
343
|
+
outside the block — including a `\r` inside a quoted value, which translation
|
|
344
|
+
would turn into a newline and split the assignment in two. The block itself is
|
|
345
|
+
written with `\n`, so a block that arrived with CRLF markers is normalised on
|
|
346
|
+
the first run and is a fixed point after that.
|
|
347
|
+
|
|
348
|
+
**The same class in `theurian init`.** `ensure_gitignore` writes an
|
|
349
|
+
identically-spelled block into a repository's `.gitignore` with the same
|
|
350
|
+
`str.find` and no count of the start markers, so a file holding two of them —
|
|
351
|
+
what resolving a merge conflict by keeping both sides leaves behind — had every
|
|
352
|
+
rule between them swallowed by the rewrite and reported as `changed: true` with
|
|
353
|
+
nothing else said. It now matches whole lines, counts the starts first, keeps a
|
|
354
|
+
CRLF `.gitignore`'s line endings, and refuses both undelimited shapes. **The
|
|
355
|
+
refusal also reaches a person differently**: it used to arrive as a Typer
|
|
356
|
+
traceback with the remedy buried in it, because the only `except` in
|
|
357
|
+
`init_command` wrapped context resolution, and it is now `error: …` plus a
|
|
358
|
+
remedy on stderr with exit 1. A refused run leaves the `.theurian/`
|
|
359
|
+
directories it had already created; nothing else is written.
|
|
360
|
+
|
|
361
|
+
**Not marked BREAKING, and here is the one place it is arguable.** No MCP tool,
|
|
362
|
+
field, type or name changed, and no published contract promised any of this
|
|
363
|
+
(see *Changing this contract* in
|
|
364
|
+
[`docs/protocol/mcp-tools.md`](../../docs/protocol/mcp-tools.md)). What did
|
|
365
|
+
change for a script is exit status on two inputs. `theurian init` over a
|
|
366
|
+
`.gitignore` holding two start markers used to exit 0 having silently eaten the
|
|
367
|
+
rules between them, and now exits 1 (an *unterminated* block already failed,
|
|
368
|
+
though as a traceback). `theurian setup` over an env file in either state used
|
|
369
|
+
to rewrite it and count the step converged; it now stops the whole run at
|
|
370
|
+
consent — exit 5, `EXIT_NEEDS_CONSENT` — before `_apply` is reached, so nothing
|
|
371
|
+
at all is written. Both are the fix, not a side effect of it.
|
|
372
|
+
|
|
373
|
+
Idempotence is now measured on the file rather than on the report: a converged
|
|
374
|
+
second run does not reopen it at all, witnessed on the mtime, so a run that
|
|
375
|
+
rewrote identical bytes fails the pin. The write goes *through* the inode
|
|
376
|
+
rather than temp-and-rename, because this file is a symlink into a dotfiles
|
|
377
|
+
repository on plenty of machines, and through an `io.BufferedWriter`, because a
|
|
378
|
+
short write here would now destroy lines Theurian did not author. 74 tests, 97
|
|
379
|
+
collected cases: `tests/unit/test_env_file_merge.py` (33 tests, including one
|
|
380
|
+
that sweeps all 363 arrangements against a rule read off the symbols rather
|
|
381
|
+
than off the code), `tests/integration/test_setup_env_file.py` (21, driving the
|
|
382
|
+
real `SetupService` over real files because the defect lived in the seam — a
|
|
383
|
+
probe asking one question while the apply performs a different write is exactly
|
|
384
|
+
what shipped, two of them — five cases — asking a real `bash` what a file
|
|
385
|
+
exports rather than asking the heuristic to agree with itself),
|
|
386
|
+
`tests/integration/test_init_gitignore_block.py` (9, through the real
|
|
387
|
+
`theurian init`), `tests/integration/test_auth_rotate.py` (6 added),
|
|
388
|
+
`tests/integration/test_setup_cli.py` (3 added, for the `doctor` and
|
|
389
|
+
`--dry-run` parity above), and `tests/integration/test_setup_service.py` (2
|
|
390
|
+
added). The last two of those close pins that were simply absent: nothing
|
|
391
|
+
asserted `CONVERGED` by value — replacing `_verify`'s state choice with an
|
|
392
|
+
unconditional `DEGRADED` passed the whole suite, because `succeeded` is true of
|
|
393
|
+
both — and nothing asserted the *status* half of the reservation test, so
|
|
394
|
+
reporting every explained `NOT_APPLICABLE` step as a warning, which would turn
|
|
395
|
+
the supply-chain note on row 3 into something wrong with this install, passed
|
|
396
|
+
it too. Four parser decision points gained pins the same way: a marker line
|
|
397
|
+
with a trailing space is not a marker, in the env file **and** in the
|
|
398
|
+
`.gitignore` scan beside it; the block is searched for before the dev0–dev2
|
|
399
|
+
rendering; markers that cannot be resolved are refused even where that
|
|
400
|
+
rendering is also present; and the probe asks currency before shadowing.
|
|
401
|
+
|
|
402
|
+
**Round two re-measured the parser rather than re-reading it.** A second sweep
|
|
403
|
+
over an extended alphabet, 2,800 shapes, found no arrangement that loses a line
|
|
404
|
+
outside the block or takes the wrong refusal decision; the two writers were
|
|
405
|
+
compared over 84 file-state combinations and produce identical bytes; and the
|
|
406
|
+
six numbers this entry publishes — 363, 229 refused, 134 merged, and the first
|
|
407
|
+
cut's 39, 16 and 19 — were re-measured exact. Those are review measurements and
|
|
408
|
+
not suite tests: what guards the rule from here on is the 363-arrangement
|
|
409
|
+
sweep, which asserts its own population size so a shrunken alphabet fails
|
|
410
|
+
rather than passing quietly.
|
|
411
|
+
|
|
412
|
+
**Two remedies now describe the state they are reached from.** The `.gitignore`
|
|
413
|
+
refusal said "Add `# <<< theurian <<<` where the block ends" to a person whose
|
|
414
|
+
file appears to have that line already — a marker is matched as a whole line,
|
|
415
|
+
so a trailing space is the likeliest way to reach an unterminated block, and
|
|
416
|
+
the remedy sent them looking for what was in front of them; it now says what
|
|
417
|
+
the line must be, trailing space and all. `auth rotate`'s `OSError` remedy
|
|
418
|
+
offered "an older block, or readable by other accounts" for a file that can
|
|
419
|
+
also be left *empty*, since the `open` truncates before the write that failed —
|
|
420
|
+
reproduced under `RLIMIT_FSIZE`, 588 bytes in and 16 out — and now admits that
|
|
421
|
+
state. A third correction is not user-visible: the comment above the rotation's
|
|
422
|
+
env-file refresh enumerated the shapes it handles, absent, stale and
|
|
423
|
+
pre-marker, without the residual — a line below the block that assigns the
|
|
424
|
+
token again survives the rotation and produces the 401 anyway, and `doctor` is
|
|
425
|
+
what reports it.
|
|
426
|
+
|
|
427
|
+
This supersedes one sentence of `0.1.0.dev2`'s `changed_paths` entry below. The
|
|
428
|
+
env file's truncation is still disclosed on the arm that motivated it, but
|
|
429
|
+
"what it replaced is preserved nowhere" no longer holds: what a completed write
|
|
430
|
+
puts back includes the lines the run did not author. What stays unpinned, as
|
|
431
|
+
there, is the window between the truncation and the write's last byte.
|
|
432
|
+
|
|
433
|
+
### Security
|
|
434
|
+
|
|
435
|
+
- **A reused `revisionId` across two items no longer leaks a withheld item's body**
|
|
436
|
+
([GHSA-7997-g35f-q59h](https://github.com/theurian/theurian/security/advisories/GHSA-7997-g35f-q59h);
|
|
437
|
+
fix commit to be linked at publication). **BREAKING (state database schema).**
|
|
438
|
+
In 0.1.0.dev0–0.1.0.dev2 a migration that reused an existing `revisionId` under
|
|
439
|
+
a second `itemId` — the shape a copy-pasted `upsertRevision` block produces —
|
|
440
|
+
pointed the second (approved) item's current revision at the first item's
|
|
441
|
+
revision row. When that first item was withheld (for example `status:
|
|
442
|
+
rejected`), its full body — title, source anchors, and any secret that caused
|
|
443
|
+
the rejection — reached `knowledge.get` and `knowledge.search` for a caller who
|
|
444
|
+
requested the *approved* item's id. Requesting the withheld id directly was
|
|
445
|
+
still correctly refused; the reuse bypassed that gate, and `migrate validate` /
|
|
446
|
+
`migrate apply` reported nothing.
|
|
447
|
+
|
|
448
|
+
**Fixed** by making `append_revision` refuse to treat a reused `revisionId` as
|
|
449
|
+
an idempotent no-op when the stored row belongs to a different item — a revision
|
|
450
|
+
id names one item for the life of a project — with a symmetric store-level guard
|
|
451
|
+
in `put_item` that refuses a `current_revision_id` naming another item's
|
|
452
|
+
revision. The state database `SCHEMA_VERSION` is bumped from 1 to 2 (an input to
|
|
453
|
+
the derived-state hash), so a state database written by an affected version — the
|
|
454
|
+
old shape, opened and served regardless of provenance — is refused on open and
|
|
455
|
+
rebuilt from the Git-tracked migrations on the next `theurian migrate apply`. The
|
|
456
|
+
derived state carries no data that is not recoverable from those migrations, so
|
|
457
|
+
the rebuild strands nothing. If the migration set itself encodes the reuse, that
|
|
458
|
+
rebuild refuses it (exit 4, naming the reused `revisionId`) until the operation
|
|
459
|
+
is given its own id. **Updating the build alone does not remediate a database an
|
|
460
|
+
affected version already wrote; run `theurian migrate apply` after upgrading.**
|
|
461
|
+
|
|
462
|
+
Affected: `theurian` 0.1.0.dev0, 0.1.0.dev1, 0.1.0.dev2. Fixed in 0.1.0.dev3.
|
|
463
|
+
|
|
464
|
+
- **The substring-search fallback's withheld-count timing channel is closed**
|
|
465
|
+
([#158](https://github.com/theurian/theurian/issues/158)), the twin of #19's
|
|
466
|
+
`knowledge.status` fix. `knowledge.search`'s unranked fallback
|
|
467
|
+
(`mcp/search.py::_scan`) used to read every item with `list_items` (`SELECT *`,
|
|
468
|
+
no status predicate) and drop the withheld rows in Python, so its response time
|
|
469
|
+
scaled with the withheld count and a caller with a stopwatch could recover by
|
|
470
|
+
subtraction exactly what `count` withholds (T-17). It now resolves the
|
|
471
|
+
surfaceable statuses and reads through `SqliteCanonicalStore.list_items_by_status`,
|
|
472
|
+
whose `status IN (...)` is forced through the `idx_items_status` index, so a
|
|
473
|
+
withheld row is never materialised and the read cost is independent of the
|
|
474
|
+
withheld count: SQLite VM steps stay flat at 119–120 across 0/50/300/1,000
|
|
475
|
+
withheld where the old scan went 63 → 913 → 5,163, and the result set is
|
|
476
|
+
byte-identical. Pinned by
|
|
477
|
+
`test_the_substring_scan_reads_items_through_idx_items_status`,
|
|
478
|
+
`test_the_substring_scan_materializes_the_same_rows_however_many_are_withheld`,
|
|
479
|
+
and `test_the_substring_scan_never_surfaces_a_retired_item_even_with_include_unapproved`
|
|
480
|
+
in `tests/integration/test_mcp_tools.py`. Two trades are recorded, not fixed: a
|
|
481
|
+
corrupt `status` cell on this path is now silently dropped by the SQL filter
|
|
482
|
+
rather than crashing the Python `may_surface` parse it replaced — the same
|
|
483
|
+
crash → silent-drop trade #19 made for `knowledge.status`, carried with that
|
|
484
|
+
integrity class as [#30](https://github.com/theurian/theurian/issues/30) — and
|
|
485
|
+
the fallback's rows-and-memory page bound stays a deferred DoS residual (T-6),
|
|
486
|
+
since bounding it changes the search fallback's published surface.
|
|
487
|
+
|
|
488
|
+
- **The unresolved-project error now bounds the `projectId` it echoes**
|
|
489
|
+
([#17](https://github.com/theurian/theurian/issues/17)), the last member of the
|
|
490
|
+
error-echo amplification class. `mcp/tools.py::_unresolvable` interpolated the
|
|
491
|
+
caller's raw `projectId` into the "not registered" message with no length bound:
|
|
492
|
+
`_resolve` runs before any `ProjectId` is constructed, so a 2,000,000-character
|
|
493
|
+
id produced a 2,000,141-character message — an ~1× amplifier of the caller's own
|
|
494
|
+
bytes. An unregistered id longer than `MAX_IDENTIFIER_LENGTH` (200, the ceiling a
|
|
495
|
+
`ProjectId` cannot exceed, duplicated in the JSON schemas as `maxLength: 200`) is
|
|
496
|
+
now reported by its length and never echoed; a well-formed unregistered id within
|
|
497
|
+
the ceiling is still named so a typo stays visible. That matches the discipline
|
|
498
|
+
`MAX_QUERY_CHARS` already holds for `query` and `ItemId` for `itemId`, so all
|
|
499
|
+
three error-echo members are bounded. Not a disclosure — the caller only ever
|
|
500
|
+
gets back bytes it sent (the `Registered:` list is the daemon's own registry
|
|
501
|
+
contents, SEC-13). Pinned by
|
|
502
|
+
`test_an_over_long_project_id_is_reported_by_length_not_echoed` and
|
|
503
|
+
`test_the_project_id_echo_is_named_up_to_the_id_ceiling_then_by_length` in
|
|
504
|
+
`tests/integration/test_mcp_tools.py`; see T-6 in
|
|
505
|
+
[the threat model](../../docs/security/threat-model.md).
|
|
506
|
+
|
|
15
507
|
## [0.1.0.dev2] - 2026-08-12
|
|
16
508
|
|
|
17
509
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: theurian
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
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.dev3"
|
|
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.dev3"
|
|
12
12
|
__protocol_version__ = CURRENT_PROTOCOL_VERSION
|
|
13
13
|
|
|
14
14
|
__all__ = ["__protocol_version__", "__version__"]
|