theurian 0.1.0.dev7__tar.gz → 0.1.0.dev9__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 (255) hide show
  1. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/.gitignore +11 -11
  2. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/CHANGELOG.md +355 -0
  3. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/PKG-INFO +2 -1
  4. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/pyproject.toml +6 -1
  5. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/__init__.py +1 -1
  6. theurian-0.1.0.dev9/src/theurian/application/proposal_service.py +1949 -0
  7. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/commands.py +30 -9
  8. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/main.py +5 -1
  9. theurian-0.1.0.dev9/src/theurian/cli/output.py +61 -0
  10. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/propose_commands.py +63 -22
  11. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/errors.py +220 -8
  12. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/migration.py +6 -0
  13. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/proposal.py +36 -6
  14. theurian-0.1.0.dev9/src/theurian/infrastructure/filesystem/migration_loader.py +1859 -0
  15. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/security/paths.py +8 -4
  16. theurian-0.1.0.dev9/tests/command_population.py +542 -0
  17. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_cli_commands.py +217 -0
  18. theurian-0.1.0.dev9/tests/integration/test_proposal_service.py +2752 -0
  19. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_propose_cli.py +428 -0
  20. theurian-0.1.0.dev9/tests/unit/test_command_population.py +763 -0
  21. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_documented_commands.py +51 -51
  22. theurian-0.1.0.dev9/tests/unit/test_dogfood_corpus_governance.py +1464 -0
  23. theurian-0.1.0.dev9/tests/unit/test_migration_loader_errors.py +3936 -0
  24. theurian-0.1.0.dev9/tests/unit/test_path_security.py +383 -0
  25. theurian-0.1.0.dev7/src/theurian/application/proposal_service.py +0 -948
  26. theurian-0.1.0.dev7/src/theurian/infrastructure/filesystem/migration_loader.py +0 -818
  27. theurian-0.1.0.dev7/tests/command_population.py +0 -210
  28. theurian-0.1.0.dev7/tests/integration/test_proposal_service.py +0 -1091
  29. theurian-0.1.0.dev7/tests/unit/test_migration_loader_errors.py +0 -1757
  30. theurian-0.1.0.dev7/tests/unit/test_path_security.py +0 -198
  31. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/LICENSE +0 -0
  32. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/README.md +0 -0
  33. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/hatch_build.py +0 -0
  34. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/__init__.py +0 -0
  35. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/forest_builder.py +0 -0
  36. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/index_builder.py +0 -0
  37. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/ingestion_service.py +0 -0
  38. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/migration_alias_guards.py +0 -0
  39. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/migration_body_guards.py +0 -0
  40. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/migration_engine.py +0 -0
  41. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/project_service.py +0 -0
  42. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/retrieval_service.py +0 -0
  43. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/setup_context.py +0 -0
  44. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/setup_service.py +0 -0
  45. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/setup_steps.py +0 -0
  46. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/setup_withholding.py +0 -0
  47. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/visibility.py +0 -0
  48. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/application/withdrawal_purge.py +0 -0
  49. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/__init__.py +0 -0
  50. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/auth_commands.py +0 -0
  51. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/context.py +0 -0
  52. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/index_commands.py +0 -0
  53. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/cli/setup_commands.py +0 -0
  54. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/daemon/__init__.py +0 -0
  55. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/daemon/instance.py +0 -0
  56. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/daemon/runner.py +0 -0
  57. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/daemon/server.py +0 -0
  58. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/__init__.py +0 -0
  59. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/chunking.py +0 -0
  60. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/compatibility.py +0 -0
  61. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/context.py +0 -0
  62. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/enums.py +0 -0
  63. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/extras.py +0 -0
  64. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/identifiers.py +0 -0
  65. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ingestion.py +0 -0
  66. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/knowledge.py +0 -0
  67. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/__init__.py +0 -0
  68. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/authorization.py +0 -0
  69. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/canonical_store.py +0 -0
  70. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/daemon_manager.py +0 -0
  71. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/determinism.py +0 -0
  72. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/embedding.py +0 -0
  73. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/index_store.py +0 -0
  74. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/mcp_client_config.py +0 -0
  75. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/object_store.py +0 -0
  76. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/reranking.py +0 -0
  77. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/review_provider.py +0 -0
  78. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/secret_store.py +0 -0
  79. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/source_parser.py +0 -0
  80. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/specification_provider.py +0 -0
  81. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/summarization.py +0 -0
  82. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ports/vector_store.py +0 -0
  83. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/project.py +0 -0
  84. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/ranking.py +0 -0
  85. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/raptor.py +0 -0
  86. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/retrieval.py +0 -0
  87. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/review.py +0 -0
  88. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/setup.py +0 -0
  89. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/specification.py +0 -0
  90. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/state.py +0 -0
  91. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/domain/values.py +0 -0
  92. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/indexing/__init__.py +0 -0
  93. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/__init__.py +0 -0
  94. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/claude/__init__.py +0 -0
  95. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/claude/mcp_config.py +0 -0
  96. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/determinism.py +0 -0
  97. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/embedding/__init__.py +0 -0
  98. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/embedding/hashing.py +0 -0
  99. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/filesystem/__init__.py +0 -0
  100. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/filesystem/parsers/markdown.py +0 -0
  101. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/filesystem/parsers/openapi.py +0 -0
  102. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/filesystem/parsers/registry.py +0 -0
  103. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/filesystem/parsers/structured.py +0 -0
  104. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/git/__init__.py +0 -0
  105. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/github/__init__.py +0 -0
  106. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/raptor/__init__.py +0 -0
  107. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/raptor/extractive.py +0 -0
  108. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/secrets/__init__.py +0 -0
  109. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/secrets/file_store.py +0 -0
  110. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/services/__init__.py +0 -0
  111. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/services/launchagent.py +0 -0
  112. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/services/runner.py +0 -0
  113. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/services/systemd_user.py +0 -0
  114. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/__init__.py +0 -0
  115. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/connection.py +0 -0
  116. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/index_forest.py +0 -0
  117. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/index_purge.py +0 -0
  118. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/index_query.py +0 -0
  119. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/index_scan.py +0 -0
  120. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/index_schema.py +0 -0
  121. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/index_store.py +0 -0
  122. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/schema.py +0 -0
  123. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/sqlite/store.py +0 -0
  124. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/infrastructure/vector/__init__.py +0 -0
  125. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/ingestion/__init__.py +0 -0
  126. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/mcp/__init__.py +0 -0
  127. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/mcp/results.py +0 -0
  128. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/mcp/search.py +0 -0
  129. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/mcp/tools.py +0 -0
  130. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/migrations/__init__.py +0 -0
  131. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/normalization/__init__.py +0 -0
  132. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/normalization/projection.py +0 -0
  133. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/observability/__init__.py +0 -0
  134. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/retrieval/__init__.py +0 -0
  135. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/review/__init__.py +0 -0
  136. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/security/__init__.py +0 -0
  137. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/security/env_file.py +0 -0
  138. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/security/tokens.py +0 -0
  139. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/security/yaml_loading.py +0 -0
  140. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/specification/__init__.py +0 -0
  141. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/src/theurian/traceability/__init__.py +0 -0
  142. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/command_extraction.py +0 -0
  143. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/conftest.py +0 -0
  144. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/fakes/__init__.py +0 -0
  145. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/fakes/clock.py +0 -0
  146. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/fakes/ids.py +0 -0
  147. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/fakes/pages.py +0 -0
  148. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/fakes/setup.py +0 -0
  149. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/fakes/store.py +0 -0
  150. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_absence_proof.py +0 -0
  151. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_alias_item_id_collision.py +0 -0
  152. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_auth_rotate.py +0 -0
  153. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_bare_install.py +0 -0
  154. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_canonical_store.py +0 -0
  155. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_canonical_store_corruption.py +0 -0
  156. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_claude_mcp_config.py +0 -0
  157. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_cli_help_without_rich.py +0 -0
  158. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_daemon.py +0 -0
  159. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_forest_builder.py +0 -0
  160. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_forest_builder_scale.py +0 -0
  161. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_forest_node_scope.py +0 -0
  162. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_forest_purge_equality.py +0 -0
  163. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_forest_purge_recompute.py +0 -0
  164. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_forest_retrieval.py +0 -0
  165. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_forest_store_retrieval.py +0 -0
  166. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_gc_during_a_search.py +0 -0
  167. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_fallback.py +0 -0
  168. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_gc_cli.py +0 -0
  169. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_purge.py +0 -0
  170. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_purge_differential.py +0 -0
  171. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_purge_nodes.py +0 -0
  172. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_scan_fold.py +0 -0
  173. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_schema_v4.py +0 -0
  174. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_index_store.py +0 -0
  175. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_ingestion.py +0 -0
  176. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_init_gitignore_block.py +0 -0
  177. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_mcp_tools.py +0 -0
  178. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_project_registry.py +0 -0
  179. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_retrieval_service.py +0 -0
  180. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_revision_id_reuse.py +0 -0
  181. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_scan_exhaustion.py +0 -0
  182. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_service_adapters.py +0 -0
  183. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_session_start_hook.py +0 -0
  184. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_setup_cli.py +0 -0
  185. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_setup_env_file.py +0 -0
  186. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_setup_journal.py +0 -0
  187. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_setup_partial_failure.py +0 -0
  188. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_setup_probe_assertions.py +0 -0
  189. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_setup_report_withholding.py +0 -0
  190. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_setup_service.py +0 -0
  191. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_short_query_retrieval.py +0 -0
  192. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_state_provenance.py +0 -0
  193. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_unreadable_registry_surface.py +0 -0
  194. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_wire_contract.py +0 -0
  195. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_withdrawal_purge.py +0 -0
  196. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/integration/test_writer_contract.py +0 -0
  197. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_artifact_integrity_claim.py +0 -0
  198. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_candidate_cut.py +0 -0
  199. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_candidate_depth.py +0 -0
  200. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_chunking.py +0 -0
  201. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_cli_help_rendering.py +0 -0
  202. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_command_extraction.py +0 -0
  203. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_compatibility.py +0 -0
  204. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_config_key_call_sites.py +0 -0
  205. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_daemon_extra.py +0 -0
  206. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_domain_invariants.py +0 -0
  207. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_env_file_merge.py +0 -0
  208. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_examples.py +0 -0
  209. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_extractive_summarizer.py +0 -0
  210. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_forest_derivation.py +0 -0
  211. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_forest_fanout.py +0 -0
  212. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_gate_call_sites.py +0 -0
  213. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_hashing_embedding.py +0 -0
  214. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_ingest_manifest_path.py +0 -0
  215. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_install_claims.py +0 -0
  216. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_layering.py +0 -0
  217. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_migration_alias_guards.py +0 -0
  218. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_migration_engine.py +0 -0
  219. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_migration_loader_identity.py +0 -0
  220. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_network_call_sites.py +0 -0
  221. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_parsers.py +0 -0
  222. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_plugin_boundary.py +0 -0
  223. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_ports.py +0 -0
  224. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_project_and_traceability.py +0 -0
  225. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_project_registry_errors.py +0 -0
  226. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_projection.py +0 -0
  227. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_proposal.py +0 -0
  228. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_ranking.py +0 -0
  229. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_raptor_scope.py +0 -0
  230. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_ref_recording.py +0 -0
  231. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_resolve_context_call_sites.py +0 -0
  232. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_result_gate_session.py +0 -0
  233. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_result_payload.py +0 -0
  234. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_retrieval_depth.py +0 -0
  235. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_schemas.py +0 -0
  236. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_scope_isolation.py +0 -0
  237. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_secret_detector.py +0 -0
  238. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_setup_changed_paths.py +0 -0
  239. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_setup_claims.py +0 -0
  240. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_setup_domain.py +0 -0
  241. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_state_hash.py +0 -0
  242. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_test_fixtures.py +0 -0
  243. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_tokens.py +0 -0
  244. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/tests/unit/test_yaml_loading.py +0 -0
  245. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/README.md +0 -0
  246. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/cli/version.schema.json +0 -0
  247. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/config/project-config.schema.json +0 -0
  248. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/knowledge/retrieval-result.schema.json +0 -0
  249. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/mcp/knowledge-search-response.schema.json +0 -0
  250. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/mcp/knowledge-status-response.schema.json +0 -0
  251. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/mcp/project-list-response.schema.json +0 -0
  252. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/mcp/retrieval-metadata.schema.json +0 -0
  253. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/mcp/tool-context.schema.json +0 -0
  254. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/migrations/migration.schema.json +0 -0
  255. {theurian-0.1.0.dev7 → theurian-0.1.0.dev9}/theurian/schemas/protocol/compatibility.schema.json +0 -0
@@ -19,17 +19,6 @@ dist/
19
19
  coverage.xml
20
20
  htmlcov/
21
21
 
22
- # --- Theurian derived artifacts -------------------------------------------
23
- # Canonical state is rebuilt from Git-tracked migrations and content.
24
- # Never commit any of these. See docs/adr/0004-sqlite-is-a-derived-artifact.md
25
- .theurian/state/
26
- .theurian/cache/
27
- .theurian/runtime/
28
- .theurian/generated/
29
- *.sqlite
30
- *.sqlite-wal
31
- *.sqlite-shm
32
-
33
22
  # --- Documentation site (derived) ---
34
23
  # `mkdocs build` writes site/. The Pages workflow copies the logo into
35
24
  # docs/assets/ before building, so anyone replicating that build locally
@@ -58,3 +47,14 @@ htmlcov/
58
47
  .env.*
59
48
  !.env.example
60
49
  *.token
50
+
51
+ # >>> theurian >>>
52
+ # Derived artifacts. Rebuilt from Git-tracked migrations (ADR-0004).
53
+ .theurian/state/
54
+ .theurian/cache/
55
+ .theurian/runtime/
56
+ .theurian/generated/
57
+ *.sqlite
58
+ *.sqlite-wal
59
+ *.sqlite-shm
60
+ # <<< theurian <<<
@@ -12,6 +12,361 @@ 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.dev9] - 2026-08-22
16
+
17
+ ### Fixed
18
+
19
+ - **`propose accept` translates every accept-path filesystem or path fault into
20
+ an `{error, remedy}` document instead of escaping a raw traceback**
21
+ ([#227](https://github.com/theurian/theurian/issues/227)). A proposal directory
22
+ this process cannot list, stat or read; a migration file it cannot open; a
23
+ `contentFile` whose path `resolve()` refuses (a NUL byte or an unpaired
24
+ surrogate); an unwritable `.theurian/migrations/` — each used to leave `accept`
25
+ as a bare `OSError`/`ValueError`, so `--json` published nothing where it
26
+ promises a document. (An unreadable `evidence.json` is not in this set: it was
27
+ already translated to an indeterminate document in 0.1.0.dev8,
28
+ [#253](https://github.com/theurian/theurian/issues/253).) Each is now a `ProposalError` naming the offending path
29
+ relative to the project root — never the absolute path, which is the machine's
30
+ home directory (SEC-7) — whose remedy sends the reader to
31
+ `.theurian/migrations/` before re-drafting: a refused read establishes nothing
32
+ about whether the migration landed, and re-drafting an accepted proposal mints a
33
+ duplicate migration (#89). A move that landed but whose trailing cleanup of the
34
+ proposal's own copied files could not run (a read-only `0o555` proposal
35
+ directory) now degrades to success with a `remedy` naming the leftover, rather
36
+ than reporting a non-landing that would send the caller to re-draft.
37
+ - **`propose accept`'s replacement guard reads the loaded migration set and keys
38
+ on the body's filesystem identity, closing a disagreement with the loader**
39
+ ([#234](https://github.com/theurian/theurian/issues/234)). The guard that
40
+ refuses a body replacement which would leave the migration set unable to
41
+ validate used to enumerate `.theurian/migrations/*.yaml` itself and skip the
42
+ symlinked entries the loader follows, so a pin held by a relocated (symlinked)
43
+ migration was invisible to it: the replacement was allowed, and the set then
44
+ stopped loading at exit 4 with no undo. It now reads the same `MigrationSet` the
45
+ loader produced, and compares on the body's `(st_dev, st_ino)` rather than a
46
+ path string.
47
+
48
+ **Behaviour change:** `accept` now refuses four inputs it previously let
49
+ through, each of which then broke the loaded set at exit 4:
50
+
51
+ - a *different* revision replacing an **unpinned** landed body — the old guard
52
+ filtered on the pin and waved this through, yet the set then holds two
53
+ revisions on one physical file (`refuse_duplicate_content_files`, exit 4);
54
+ - a byte-identical replacement of a **pinned** landed body under a *different*
55
+ revision id — the old guard treated "byte-identical changes nothing" as true
56
+ of the *set*, but it is the same two-revisions-on-one-file break;
57
+ - a **hardlink** to a landed body — the identity key `(st_dev, st_ino)` reaches
58
+ the same inode by every alias, where the old path-string compare saw none;
59
+ - a **case variant** of a landed body's path (`RETRY-POLICY.md` against a landed
60
+ `retry-policy.md`) — likewise one inode by every spelling. A Unicode
61
+ normalisation variant reaches it the same way, though no test exercises that
62
+ face.
63
+
64
+ The one in-place re-declare `accept` still allows — this proposal's own revision
65
+ re-declared byte-for-byte **on the same item**, the in-place status change of
66
+ ADR-0024 decision 5 — is an explicit conjunction of equal item id, revision id
67
+ and bytes. A byte-*different* re-declare of a *pinned* landed revision was
68
+ already refused before this branch and is unchanged. There are no users yet, but
69
+ the refusals change what `accept` exits on, so they are named here.
70
+ - **`propose accept` refuses a cross-item re-declare of a landed revision id**
71
+ (round-one review finding). The replacement-guard skip that admits the one
72
+ legitimate in-place re-declare (ADR-0024 decision 5) fired on two conjuncts —
73
+ equal revision id and equal bytes — so a byte-identical body re-declared under a
74
+ *different* item's id passed it. `accept` returned 0, and `migrate validate`
75
+ passed too (it does not check cross-item revision ownership); then `migrate
76
+ apply` refused the whole set at exit 4 ("a revision id belongs to one item",
77
+ INV-1/SEC-13) after the pull request had merged and the proposal was already
78
+ consumed, with no undo. The skip now carries the item id as a third conjunct
79
+ (item id, revision id, bytes), moving that refusal to the accept door. No
80
+ rejected-item content is disclosed either way — `store.py` refuses a cross-item
81
+ revision-id reuse before it reads content (SEC-13) — so this spares the operator
82
+ a consumed proposal; it does not add a disclosure control.
83
+ - **`propose accept` refuses a migration whose id the loaded set already holds
84
+ under another filename** (round-one review finding). The "already in place"
85
+ refusal keyed only on the destination filename (`<id>-<slug>.yaml`), but the
86
+ loader keys migrations by their *inner* `id`. A hand-authored proposal named
87
+ `<landed-id>-other-slug.yaml` carrying `id: <landed-id>` collided on the inner
88
+ id while its filename was free: the name check waved it through, the
89
+ filename/id agreement check was satisfied (the prefix equals the id), and
90
+ `accept` landed a *duplicate* migration id, on which `migrate
91
+ validate`/`status`/`apply` then all exit 4. The refusal now also answers the id
92
+ against the same loaded migration set the loader, `migrate validate` and
93
+ `migrate apply` read (#234/#253/#254). This is the third and last accept-path
94
+ procedure moved off a filesystem enumeration and onto the loaded set; no method
95
+ on the accept path enumerates `.theurian/migrations/` any more.
96
+ - **The accept-path read-failure remedy is chosen by `errno`, not a blanket
97
+ `chmod`** (round-one review finding, extending
98
+ [#227](https://github.com/theurian/theurian/issues/227)). The remedy prescribed
99
+ "Make _path_ readable — chmod u+rX on it" for every accept-path read failure —
100
+ the same over-claim [#233](https://github.com/theurian/theurian/issues/233)
101
+ corrected for `PathEscapeError`, reopened here. A `contentFile` naming a
102
+ directory raises `EISDIR`, which no `chmod` cures: the fault is the authored
103
+ input, so the remedy now names the `contentFile` to correct and says nothing
104
+ about permissions. A `stat`/`open` refused for a *child* is the parent directory
105
+ lacking its search bit, so `chmod u+rX` on the child cures the wrong file — the
106
+ remedy now points `chmod u+x` at the unsearchable directory instead.
107
+ `EACCES`/`EPERM` still earn a `chmod`; a `None` errno is treated as
108
+ non-permission, since a `chmod` prescribed for an unknown cause is the very
109
+ over-claim this avoids. Every branch still points at `.theurian/migrations/`
110
+ before any re-draft (#89).
111
+ - **Scope note, not a behaviour change: `propose accept` does not self-validate
112
+ the proposal it is accepting**
113
+ ([#307](https://github.com/theurian/theurian/issues/307)). The replacement
114
+ guard's docstring claimed it holds the invariant that `accept` "never leaves the
115
+ set unable to validate." That is false as a global invariant: `accept` does not
116
+ schema-validate the incoming migration and does not check it against itself, so
117
+ a self-contained breakage in a single proposal — two operations naming one
118
+ `contentFile`, a self-inconsistent pin, an empty `contentFile` — lands and is
119
+ caught by `migrate validate` in CI, which is the check by design (ADR-0013 §4).
120
+ The docstring is narrowed to what the guard actually holds: it refuses a
121
+ replacement that would break a pin *already landed* in the approved set, the one
122
+ fault it can judge from the landed set alone. No behaviour changed here;
123
+ hardening `accept` itself against a self-contained breakage is deferred to #307.
124
+ - **A migration-schema rejection echoed `jsonschema`'s raw `{instance!r}` and
125
+ had dropped the schema-side fact that names the defect**
126
+ ([#289](https://github.com/theurian/theurian/issues/289)). When a migration
127
+ failed validation, the message interpolated the whole failing instance
128
+ verbatim — unbounded, and with control characters unescaped — while no longer
129
+ saying *what the schema expected*: the `const` a value must equal, the
130
+ `pattern` it must match, or the unexpected keys an `additionalProperties:
131
+ false` rejected. The rejection is now built at Theurian's own seam
132
+ (`migration_loader.py`'s `_schema_rejection`), keeping that schema-side fact
133
+ and bounding every author-written fragment it echoes to `MAX_ECHOED_VALUE`
134
+ (1,000 characters) through a `reprlib`-based renderer that escapes control
135
+ characters. This restores the diagnostic parity the pre-change wording had
136
+ while removing the unbounded echo — a 100 KB value no longer renders a
137
+ 100,198-character refusal into a terminal.
138
+
139
+ - **An oversized scalar nested in a rejected operation reported its
140
+ post-truncation length, not its true length**
141
+ ([#289](https://github.com/theurian/theurian/issues/289)). When a rejected
142
+ operation carried an oversized string, the echo truncated the container's
143
+ render first, so the "N characters in all" count reported the *container
144
+ repr's* length (~1,100), not the offending value's. The renderer now records
145
+ the longest scalar's pre-truncation length and the rejection names that true
146
+ length — the one number that is the diagnosis for a value refused for being
147
+ large.
148
+
149
+ - **A hostile migration document could exhaust CPU or memory at validate time,
150
+ and a giant-integer scalar escaped `--json` as a raw traceback**
151
+ ([#291](https://github.com/theurian/theurian/issues/291)). Several shapes of a
152
+ parsed migration document defeated `jsonschema`'s own message building at
153
+ validate time. The loader now applies three bounds ahead of `validate` in its
154
+ own un-memoised walk — nesting depth, node count, and total rendered magnitude
155
+ — and translates a single giant integer by type after `validate` raises:
156
+ - **Nesting** is refused ahead of `validate` at `MAX_DOCUMENT_NESTING` (64).
157
+ Past the interpreter's C recursion budget `jsonschema` cannot build its own
158
+ refusal message, and the `RecursionError` that follows is indistinguishable
159
+ from a corrupt schema — so a deep *document* was answered "reinstall
160
+ theurian". A schema-valid migration nests at most 7 levels.
161
+ - **Branching alias expansion** is refused ahead of `validate` at
162
+ `MAX_DOCUMENT_NODES` (100,000). A YAML anchor aliased into a doubling chain
163
+ is a ~500-byte file whose expansion is 2^N nodes; `jsonschema`'s
164
+ `{instance!r}` re-expands it the same way, building a 46 MB message from a
165
+ 500-byte file at alias level 22 (measured 2026-08-21, `jsonschema` 4.26.0).
166
+ The walk deliberately does not collapse shared references — a collapsed
167
+ count would wave the bomb through. Same un-memoised-walk shape as the OpenAPI
168
+ `$ref` ref-walk in
169
+ [#245](https://github.com/theurian/theurian/issues/245), in another seam.
170
+ - **Total rendered magnitude** is refused ahead of `validate` at
171
+ `MAX_DOCUMENT_RENDERED_CHARS` (1,000,000) — a new bound in the same walk that
172
+ the node count could not stand in for. One large scalar aliased into many
173
+ slots is only a handful of nodes, well under `MAX_DOCUMENT_NODES`, but
174
+ `{instance!r}` re-expands it to N times its rendered width. The walk charges
175
+ every leaf's width via a new O(1) `_rendered_width` per un-memoised reference
176
+ — `len` for a `str`/`bytes`, a `bit_length`-derived decimal-digit estimate
177
+ for an `int` (never `str(int)`, which is quadratic and raises past CPython's
178
+ int→str limit), and the `repr` length of a bounded `bool`/`float`/`None`.
179
+ This closes two aliased-scalar faces. The first is a large *string* aliased
180
+ into N slots: a hundreds-of-gigabytes transient that raises `MemoryError`,
181
+ which is neither a `ValueError` nor an `ArithmeticError` and so escaped the
182
+ scalar catch below as a raw traceback. The second was found by the round-two
183
+ review and missed by the round-one bound, which charged only `str`/`bytes`: a
184
+ *medium integer* — a few thousand digits, so its own `repr` does not raise —
185
+ aliased into N slots is O(N) nodes under `MAX_DOCUMENT_NODES` and not a
186
+ `ValueError`, so it defeated every round-one control while `{instance!r}`
187
+ re-emitted its digits once per slot, a `digits × slots`-character message.
188
+ Charging every leaf per reference refuses both faces before `validate`. The
189
+ refusal message still says "characters of string content", but the bound now
190
+ covers every leaf type, integers and bytes included.
191
+ - **A single giant integer** — one whose own `{instance!r}` render raises,
192
+ past CPython's int→str limit (4300 digits by default) — is one node whose
193
+ lone width passes the aggregate budget above, so it reaches `validate` and is
194
+ *translated by type* rather than refused ahead. `jsonschema` renders it past
195
+ that limit and raises `ValueError` (reachable today), or a float `multipleOf`
196
+ coerces it and raises `OverflowError` (latent — the bundled schema carries
197
+ only `minimum`/`maximum`). `_validate_document` catches the whole
198
+ `(ValueError, ArithmeticError)` class as a `MigrationError` so a future
199
+ numeric keyword cannot reopen the escape. This catch and the rendered budget
200
+ are complementary, not redundant: the budget refuses the *aliased* integer
201
+ whose aggregate render is large, and this catch handles the *single* integer
202
+ whose lone render raises. The file-load path closes the same single-value
203
+ face: a YAML integer literal past CPython's limit is now translated to the
204
+ same bounded "reduce it" wording instead of forwarding CPython's message,
205
+ which named `sys.set_int_max_str_digits()` — an interpreter tuning knob no
206
+ migration author should reach for. As defence in depth, the rejection
207
+ builder's `_echo` refuses an integer wider than `_MAX_ECHOED_INT_BITS`
208
+ (2,000 bits) as a placeholder.
209
+
210
+ ## [0.1.0.dev8] - 2026-08-20
211
+
212
+ ### Added
213
+
214
+ - **`evidence.json` now records the `migrationId` and `itemId` its proposal
215
+ drafted** ([#253](https://github.com/theurian/theurian/issues/253)). These are
216
+ the two fields of that file Core reads back: together they let `propose accept`
217
+ ask whether a migration with that id, operating on that item, is in
218
+ `.theurian/migrations/`. Both are a contributor's claim, not authority —
219
+ `evidence.json` is committed and untrusted (ADR-0013 point 7) — so `itemId` is
220
+ the cross-check that stops a forged `migrationId` (one pointing at another
221
+ proposal's landed migration) from reading as accepted. Optional on read: the 26
222
+ proposals committed in this repository predate both fields and are diagnosed by
223
+ best-effort inference, which their message says.
224
+
225
+ ### Fixed
226
+
227
+ - **The installed migration schema resolved `$ref`s over the network, and its
228
+ resolution failures escaped as raw tracebacks**
229
+ ([#235](https://github.com/theurian/theurian/issues/235)). The migration
230
+ schema was validated with `jsonschema`'s default registry, whose retrieve
231
+ callable fetches an external `$ref` over the network
232
+ (`urllib.request.urlopen`) at validate time — and reads a `file://` ref off
233
+ disk the same way. An SSRF-shaped read gated only on the installed schema
234
+ being corrupted or replaced (an operator-side precondition; `schema_root()`
235
+ never reads user-project paths), and covered by no existing claim:
236
+ `parsers/openapi.py`'s "external `$ref` targets are recorded, never fetched"
237
+ governs *ingested* documents, not the schema this build ships. Every
238
+ validator is now built with `referencing`'s `EMPTY_REGISTRY` — no retrieve
239
+ callable — so an external `$ref` fails closed; internal `#/$defs/…` refs
240
+ still resolve from the schema's own root resource, so the bundled schema
241
+ validates unchanged. Separately, the resulting resolution failures — an
242
+ unresolvable `$ref`/`$dynamicRef` raises `referencing.exceptions.Unresolvable`
243
+ and a self-recursive or empty `$ref` raises `RecursionError`, neither a
244
+ `ValidationError` — used to slip past every `except ValidationError` seam and
245
+ reach `resolve_context` as a raw traceback under `--json`. Both are now
246
+ translated to `SchemaUnreadableError`, the type every other
247
+ installed-schema-corruption failure already carries, at both validate seams
248
+ (`migrate validate` / `propose`, and any command that loads migrations from
249
+ disk).
250
+ - **A path-containment refusal told the user to go somewhere they already were,
251
+ and could instruct them to delete their own work**
252
+ ([#233](https://github.com/theurian/theurian/issues/233)). With
253
+ `.theurian/migrations` — or one `*.yaml` file inside it — a symlink pointing
254
+ outside the project, `theurian migrate validate --json` printed
255
+ `{"error": "Path escapes the permitted root /<absolute>/<project>", "remedy":
256
+ "Run this inside an initialised Theurian project."}` at exit 1. The refusal
257
+ itself was correct and is unchanged; what it said about itself was not.
258
+ `PathEscapeError` set no `remedy` of its own, so the CLI's generic fallback
259
+ won — the "exception that does not describe itself" shape
260
+ [#205](https://github.com/theurian/theurian/issues/205) exists to end. It now
261
+ names, relative to the project root and with the `!r` quoting its siblings
262
+ use, *where* the problem is — and **no remedy names a file to delete.** That
263
+ is the substantive change: an escape happens through a symbolic link somewhere
264
+ on the entry's ancestor chain or its resolution chain, and which link it is
265
+ cannot be determined from the entry alone. Three successive attempts to
266
+ determine it anyway were each refuted by a deeper construction — the last by
267
+ `x.yaml → y.yaml → outside`, where following the remedy deleted two
268
+ Git-tracked files and ended at `valid: true` while the minimal cure was
269
+ repointing `y` alone. So the remedy now states only what `lstat` proves — that
270
+ the named entry is or is not itself a link — and then hands over the finite
271
+ checklist: this entry, the directories above it, the links it resolves
272
+ through. "Repoint that link, or remove that link" refers to whichever the
273
+ reader finds; Theurian never names it. Related: an outside-pointing
274
+ `.theurian` is reachable from a plain `git clone`
275
+ ([#237](https://github.com/theurian/theurian/issues/237)). The offending path
276
+ is still never echoed (SEC-7), and neither is the absolute project root.
277
+ - **A `contentFile` refusal no longer denies the commonest cure.** The
278
+ path-escape remedy for a `contentFile` used to say that removing the `..` was
279
+ "not the cure" — wrong for a plain over-traversal typo, which involves no
280
+ symlink at all and is fixed by exactly that edit, and in conflict with the
281
+ sibling `MigrationContentUnreadableError`, which tells the author to fix the
282
+ path. It now offers both candidates: correct the path, or find the link
283
+ something it traverses goes through.
284
+ - **Exit code:** a `PathEscapeError` raised while resolving a project now exits
285
+ `EXIT_STATE_ERROR` (4) rather than 1 for the commands routed through
286
+ `_require_project` — measured, all nine: `migrate validate`, `migrate status`,
287
+ `migrate apply`, `index build`, `index status`, `index gc`, `ingest`,
288
+ `propose` and `propose accept` — matching every sibling refusal on the same
289
+ load path (`MigrationsDirectoryUnreadableError`,
290
+ `MigrationFileUnreadableError`, `MigrationError`). A script keying on exit 1
291
+ for those must be updated. `init` and `project register` call
292
+ `resolve_context` directly and still exit 1; `project status` reports at exit
293
+ 0 as it always has. All three now carry the new remedy text.
294
+ - **A path refused only for nesting too deep no longer claims to have escaped**
295
+ ([#233](https://github.com/theurian/theurian/issues/233)). `resolve_within_root`
296
+ refuses a path past `MAX_PATH_DEPTH` whether or not it ever leaves the root,
297
+ and reported it as `Path escapes the permitted root` — false for a path
298
+ sitting entirely inside. It now raises `PathDepthExceededError` with its own
299
+ message and a remedy about flattening the nesting. The new type is a
300
+ `PathEscapeError` subclass, so every existing `except` and every exit-code
301
+ route catches it unchanged; only what the caller is told differs.
302
+ - **An interrupted `theurian propose` was diagnosed as an accepted one**
303
+ ([#253](https://github.com/theurian/theurian/issues/253)). `propose accept`
304
+ read the presence of `evidence.json` as "this proposal has already been
305
+ accepted", but `propose` writes the body, then the evidence, then the
306
+ migration — so a run killed before its last write left exactly that shape.
307
+ `accept` reported that the migration had been moved into
308
+ `.theurian/migrations/` and that no action was needed beyond opening a pull
309
+ request, while `.theurian/migrations/` held nothing and the drafted knowledge
310
+ existed nowhere. The remedy discarded the draft.
311
+
312
+ Acceptance is a **best-effort diagnosis over untrusted input, not a
313
+ tamper-proof fact**: the proposal directory is contributor-controlled, so the
314
+ recorded `migrationId` is a claim (cross-checked by `itemId` against the
315
+ migration it names), a `evidence.json` that is present but unreadable is
316
+ answered as *indeterminate* rather than collapsed into "no record", and every
317
+ fallible branch points the reader at `.theurian/migrations/` first — no branch
318
+ emits an unconditional "no action is needed" or "draft it again", so none can
319
+ tell the author to discard work that may exist or duplicate a change that
320
+ already landed. Whether a migration is in place under the recorded id is read
321
+ from the same approved `MigrationSet` `migrate validate`/`apply` read — keyed by
322
+ the migration's inner id, not by a filename match — so `propose accept` cannot
323
+ disagree with the loader about what has landed, whatever a migration file was
324
+ renamed to or whether it is a symlink. The three states are kept distinct by
325
+ their exit codes: a migration in place under the recorded id — whether or not
326
+ the item cross-checks — exits 4 ("read before acting"), and only a recorded id
327
+ the loaded set holds *no* migration for exits 1 ("nothing landed, re-draft"), so
328
+ following exit 1 can never mint a duplicate of a change on disk. Absent
329
+ `evidence.json` (a legacy proposal, or
330
+ one interrupted before its evidence write) falls to inference over the
331
+ directory, which reads only the body shape the generator produces, so a
332
+ reviewer's notes or a `Thumbs.db` left beside an accepted proposal no longer
333
+ flips the verdict.
334
+
335
+ - **A committed proposal could forge `theurian propose accept`'s own output**
336
+ ([#253](https://github.com/theurian/theurian/issues/253)). A proposal directory
337
+ arrives through a contributor's pull request, and a file name or a migration's
338
+ `contentFile` carrying `ESC [ 2 K` and a carriage return erases the line the
339
+ terminal has drawn and prints its own in place of it — reproduced printing this
340
+ command's own output under its own name, on both the refusal path and the
341
+ exit-0 **success** payload (`bodyFiles`, `migrationFile`). The CLI now escapes
342
+ every terminal-control character — the whole C0 block, `DEL` and C1 — at one
343
+ shared sink that every text-mode emitter routes each value and key through, so
344
+ no value any command prints, from any source, reaches a terminal with a raw
345
+ control byte. A value's own newline is escaped too (the output's structural
346
+ newlines are the emitters' own), while printable Unicode (a Japanese title) is
347
+ kept. The `--json` output was never affected.
348
+
349
+ ### Changed
350
+
351
+ - **BREAKING — re-accepting an already-accepted proposal now exits 4, not 1**
352
+ ([#254](https://github.com/theurian/theurian/issues/254)). The published exit
353
+ code table documented 4 for "that migration is already in place", while the
354
+ natural route to that state — running `theurian propose accept` twice — exited
355
+ 1 alongside "no such proposal", an interrupted draft, and a refused
356
+ `contentFile`. Exit 4 keeps its meaning and gains this case: *the knowledge
357
+ state refuses this move, so read it before acting*. It is not "already done" —
358
+ an approved migration set that cannot be read also exits 4, with the proposal
359
+ still waiting — and the published table now says so rather than promising that
360
+ 1 always means nothing landed. Scripts that treat any non-zero exit as failure
361
+ are unaffected; one that special-cased 1 for "already accepted" must read 4.
362
+
363
+ Breaking by the compatibility table in
364
+ [`docs/protocol/plugin-core-compatibility.md`](../../docs/protocol/plugin-core-compatibility.md),
365
+ and `protocolVersion` is **not** bumped for it — a recorded, narrowly scoped
366
+ exemption on the same grounds as `system.capabilities.milestone`'s (#206),
367
+ written up under "Changing this contract" in
368
+ [`docs/protocol/mcp-tools.md`](../../docs/protocol/mcp-tools.md).
369
+
15
370
  ## [0.1.0.dev7] - 2026-08-19
16
371
 
17
372
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: theurian
3
- Version: 0.1.0.dev7
3
+ Version: 0.1.0.dev9
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
@@ -22,6 +22,7 @@ Requires-Dist: jsonschema==4.26.0
22
22
  Requires-Dist: pydantic==2.13.4
23
23
  Requires-Dist: python-ulid==4.0.1
24
24
  Requires-Dist: pyyaml==6.0.3
25
+ Requires-Dist: referencing==0.37.0
25
26
  Requires-Dist: typer==0.27.1
26
27
  Provides-Extra: all
27
28
  Requires-Dist: mcp==2.0.0; extra == 'all'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "theurian"
7
- version = "0.1.0.dev7"
7
+ version = "0.1.0.dev9"
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"
@@ -36,6 +36,11 @@ dependencies = [
36
36
  "pydantic==2.13.4",
37
37
  "python-ulid==4.0.1",
38
38
  "pyyaml==6.0.3",
39
+ # jsonschema's public reference-resolution API, imported directly for the
40
+ # no-network registry that keeps the installed migration schema offline
41
+ # (issue #235). A transitive dep of jsonschema, declared here because we
42
+ # import it -- ADR-0014's "import what you declare".
43
+ "referencing==0.37.0",
39
44
  "typer==0.27.1",
40
45
  ]
41
46
 
@@ -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.dev7"
11
+ __version__ = "0.1.0.dev9"
12
12
  __protocol_version__ = CURRENT_PROTOCOL_VERSION
13
13
 
14
14
  __all__ = ["__protocol_version__", "__version__"]