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.
Files changed (247) hide show
  1. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/CHANGELOG.md +96 -3
  2. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/PKG-INFO +1 -1
  3. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/pyproject.toml +1 -1
  4. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/__init__.py +1 -1
  5. theurian-0.1.0.dev7/src/theurian/application/migration_alias_guards.py +183 -0
  6. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/migration_engine.py +14 -5
  7. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/proposal_service.py +20 -4
  8. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/commands.py +55 -8
  9. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/propose_commands.py +180 -6
  10. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/errors.py +46 -0
  11. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/migration.py +12 -4
  12. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/canonical_store.py +16 -0
  13. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/migration_loader.py +6 -4
  14. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/store.py +18 -0
  15. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/tools.py +28 -12
  16. theurian-0.1.0.dev7/tests/integration/test_alias_item_id_collision.py +687 -0
  17. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_proposal_service.py +4 -2
  18. theurian-0.1.0.dev7/tests/integration/test_propose_cli.py +989 -0
  19. theurian-0.1.0.dev7/tests/unit/test_migration_alias_guards.py +183 -0
  20. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_result_gate_session.py +13 -0
  21. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_retrieval_depth.py +5 -0
  22. theurian-0.1.0.dev5/tests/integration/test_propose_cli.py +0 -386
  23. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/.gitignore +0 -0
  24. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/LICENSE +0 -0
  25. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/README.md +0 -0
  26. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/hatch_build.py +0 -0
  27. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/__init__.py +0 -0
  28. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/forest_builder.py +0 -0
  29. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/index_builder.py +0 -0
  30. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/ingestion_service.py +0 -0
  31. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/migration_body_guards.py +0 -0
  32. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/project_service.py +0 -0
  33. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/retrieval_service.py +0 -0
  34. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_context.py +0 -0
  35. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_service.py +0 -0
  36. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_steps.py +0 -0
  37. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/setup_withholding.py +0 -0
  38. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/visibility.py +0 -0
  39. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/application/withdrawal_purge.py +0 -0
  40. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/__init__.py +0 -0
  41. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/auth_commands.py +0 -0
  42. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/context.py +0 -0
  43. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/index_commands.py +0 -0
  44. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/main.py +0 -0
  45. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/cli/setup_commands.py +0 -0
  46. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/__init__.py +0 -0
  47. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/instance.py +0 -0
  48. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/runner.py +0 -0
  49. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/daemon/server.py +0 -0
  50. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/__init__.py +0 -0
  51. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/chunking.py +0 -0
  52. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/compatibility.py +0 -0
  53. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/context.py +0 -0
  54. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/enums.py +0 -0
  55. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/extras.py +0 -0
  56. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/identifiers.py +0 -0
  57. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ingestion.py +0 -0
  58. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/knowledge.py +0 -0
  59. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/__init__.py +0 -0
  60. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/authorization.py +0 -0
  61. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/daemon_manager.py +0 -0
  62. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/determinism.py +0 -0
  63. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/embedding.py +0 -0
  64. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/index_store.py +0 -0
  65. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/mcp_client_config.py +0 -0
  66. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/object_store.py +0 -0
  67. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/reranking.py +0 -0
  68. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/review_provider.py +0 -0
  69. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/secret_store.py +0 -0
  70. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/source_parser.py +0 -0
  71. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/specification_provider.py +0 -0
  72. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/summarization.py +0 -0
  73. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ports/vector_store.py +0 -0
  74. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/project.py +0 -0
  75. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/proposal.py +0 -0
  76. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/ranking.py +0 -0
  77. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/raptor.py +0 -0
  78. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/retrieval.py +0 -0
  79. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/review.py +0 -0
  80. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/setup.py +0 -0
  81. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/specification.py +0 -0
  82. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/state.py +0 -0
  83. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/domain/values.py +0 -0
  84. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/indexing/__init__.py +0 -0
  85. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/__init__.py +0 -0
  86. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/claude/__init__.py +0 -0
  87. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/claude/mcp_config.py +0 -0
  88. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/determinism.py +0 -0
  89. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/embedding/__init__.py +0 -0
  90. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/embedding/hashing.py +0 -0
  91. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/__init__.py +0 -0
  92. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/markdown.py +0 -0
  93. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/openapi.py +0 -0
  94. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/registry.py +0 -0
  95. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/filesystem/parsers/structured.py +0 -0
  96. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/git/__init__.py +0 -0
  97. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/github/__init__.py +0 -0
  98. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/raptor/__init__.py +0 -0
  99. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/raptor/extractive.py +0 -0
  100. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/secrets/__init__.py +0 -0
  101. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/secrets/file_store.py +0 -0
  102. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/__init__.py +0 -0
  103. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/launchagent.py +0 -0
  104. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/runner.py +0 -0
  105. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/services/systemd_user.py +0 -0
  106. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/__init__.py +0 -0
  107. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/connection.py +0 -0
  108. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_forest.py +0 -0
  109. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_purge.py +0 -0
  110. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_query.py +0 -0
  111. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_scan.py +0 -0
  112. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_schema.py +0 -0
  113. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/index_store.py +0 -0
  114. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/sqlite/schema.py +0 -0
  115. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/infrastructure/vector/__init__.py +0 -0
  116. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/ingestion/__init__.py +0 -0
  117. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/__init__.py +0 -0
  118. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/results.py +0 -0
  119. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/mcp/search.py +0 -0
  120. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/migrations/__init__.py +0 -0
  121. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/normalization/__init__.py +0 -0
  122. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/normalization/projection.py +0 -0
  123. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/observability/__init__.py +0 -0
  124. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/retrieval/__init__.py +0 -0
  125. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/review/__init__.py +0 -0
  126. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/__init__.py +0 -0
  127. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/env_file.py +0 -0
  128. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/paths.py +0 -0
  129. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/tokens.py +0 -0
  130. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/security/yaml_loading.py +0 -0
  131. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/specification/__init__.py +0 -0
  132. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/src/theurian/traceability/__init__.py +0 -0
  133. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/command_extraction.py +0 -0
  134. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/command_population.py +0 -0
  135. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/conftest.py +0 -0
  136. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/__init__.py +0 -0
  137. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/clock.py +0 -0
  138. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/ids.py +0 -0
  139. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/pages.py +0 -0
  140. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/setup.py +0 -0
  141. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/fakes/store.py +0 -0
  142. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_absence_proof.py +0 -0
  143. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_auth_rotate.py +0 -0
  144. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_bare_install.py +0 -0
  145. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_canonical_store.py +0 -0
  146. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_canonical_store_corruption.py +0 -0
  147. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_claude_mcp_config.py +0 -0
  148. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_cli_commands.py +0 -0
  149. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_cli_help_without_rich.py +0 -0
  150. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_daemon.py +0 -0
  151. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_builder.py +0 -0
  152. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_builder_scale.py +0 -0
  153. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_node_scope.py +0 -0
  154. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_purge_equality.py +0 -0
  155. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_purge_recompute.py +0 -0
  156. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_retrieval.py +0 -0
  157. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_forest_store_retrieval.py +0 -0
  158. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_gc_during_a_search.py +0 -0
  159. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_fallback.py +0 -0
  160. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_gc_cli.py +0 -0
  161. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_purge.py +0 -0
  162. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_purge_differential.py +0 -0
  163. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_purge_nodes.py +0 -0
  164. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_scan_fold.py +0 -0
  165. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_schema_v4.py +0 -0
  166. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_index_store.py +0 -0
  167. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_ingestion.py +0 -0
  168. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_init_gitignore_block.py +0 -0
  169. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_mcp_tools.py +0 -0
  170. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_project_registry.py +0 -0
  171. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_retrieval_service.py +0 -0
  172. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_revision_id_reuse.py +0 -0
  173. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_scan_exhaustion.py +0 -0
  174. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_service_adapters.py +0 -0
  175. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_session_start_hook.py +0 -0
  176. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_cli.py +0 -0
  177. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_env_file.py +0 -0
  178. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_journal.py +0 -0
  179. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_partial_failure.py +0 -0
  180. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_probe_assertions.py +0 -0
  181. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_report_withholding.py +0 -0
  182. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_setup_service.py +0 -0
  183. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_short_query_retrieval.py +0 -0
  184. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_state_provenance.py +0 -0
  185. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_unreadable_registry_surface.py +0 -0
  186. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_wire_contract.py +0 -0
  187. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_withdrawal_purge.py +0 -0
  188. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/integration/test_writer_contract.py +0 -0
  189. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_artifact_integrity_claim.py +0 -0
  190. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_candidate_cut.py +0 -0
  191. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_candidate_depth.py +0 -0
  192. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_chunking.py +0 -0
  193. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_cli_help_rendering.py +0 -0
  194. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_command_extraction.py +0 -0
  195. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_compatibility.py +0 -0
  196. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_config_key_call_sites.py +0 -0
  197. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_daemon_extra.py +0 -0
  198. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_documented_commands.py +0 -0
  199. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_domain_invariants.py +0 -0
  200. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_env_file_merge.py +0 -0
  201. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_examples.py +0 -0
  202. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_extractive_summarizer.py +0 -0
  203. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_forest_derivation.py +0 -0
  204. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_forest_fanout.py +0 -0
  205. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_gate_call_sites.py +0 -0
  206. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_hashing_embedding.py +0 -0
  207. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ingest_manifest_path.py +0 -0
  208. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_install_claims.py +0 -0
  209. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_layering.py +0 -0
  210. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_migration_engine.py +0 -0
  211. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_migration_loader_errors.py +0 -0
  212. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_migration_loader_identity.py +0 -0
  213. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_network_call_sites.py +0 -0
  214. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_parsers.py +0 -0
  215. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_path_security.py +0 -0
  216. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_plugin_boundary.py +0 -0
  217. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ports.py +0 -0
  218. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_project_and_traceability.py +0 -0
  219. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_project_registry_errors.py +0 -0
  220. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_projection.py +0 -0
  221. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_proposal.py +0 -0
  222. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ranking.py +0 -0
  223. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_raptor_scope.py +0 -0
  224. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_ref_recording.py +0 -0
  225. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_resolve_context_call_sites.py +0 -0
  226. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_result_payload.py +0 -0
  227. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_schemas.py +0 -0
  228. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_scope_isolation.py +0 -0
  229. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_secret_detector.py +0 -0
  230. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_setup_changed_paths.py +0 -0
  231. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_setup_claims.py +0 -0
  232. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_setup_domain.py +0 -0
  233. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_state_hash.py +0 -0
  234. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_test_fixtures.py +0 -0
  235. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_tokens.py +0 -0
  236. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/tests/unit/test_yaml_loading.py +0 -0
  237. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/README.md +0 -0
  238. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/cli/version.schema.json +0 -0
  239. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/config/project-config.schema.json +0 -0
  240. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/knowledge/retrieval-result.schema.json +0 -0
  241. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/knowledge-search-response.schema.json +0 -0
  242. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/knowledge-status-response.schema.json +0 -0
  243. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/project-list-response.schema.json +0 -0
  244. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/retrieval-metadata.schema.json +0 -0
  245. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/mcp/tool-context.schema.json +0 -0
  246. {theurian-0.1.0.dev5 → theurian-0.1.0.dev7}/theurian/schemas/migrations/migration.schema.json +0 -0
  247. {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
- <!-- Advisory (GHSA) reference for T-19 to be filled at release, when the
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.dev5
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.dev5"
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.dev5"
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 last of the three and over the same
336
- whole set: the two above name a specific migration as wrong,
337
- while this one is a statement about the set, and reporting the
338
- narrower fault first is what keeps a reader from being sent to
339
- a second migration that is not the one to edit.
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 deliberately absent -- claiming ``reviewed`` on
844
- something an agent drafted asserts a review that has not happened, and a
845
- reviewer can add either.
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
- Both gating rules feed `refusedIds`: the tenant/ACL scope rule (issue #63)
341
- and the one-body-one-revision rule (issue #210). Each has a non-throwing
342
- enumerator split from its throwing refusal precisely so `status` can report
343
- without gating -- reporting only the scope rule, as this did, told a reader
344
- `refusedIds: []` for a set `validate`/`apply` exit 4 on. Deduplicated in
345
- first-seen order (a migration can carry both faults), so the field is stable.
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 this too, but only once a write
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 this rule too (issue #210).
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