theurian 0.1.0.dev2__tar.gz → 0.1.0.dev3__tar.gz

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