utterplan 0.6.8__tar.gz → 0.6.11__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 (253) hide show
  1. {utterplan-0.6.8 → utterplan-0.6.11}/.github/workflows/python-publish.yml +1 -1
  2. {utterplan-0.6.8 → utterplan-0.6.11}/.github/workflows/tests.yml +1 -1
  3. {utterplan-0.6.8 → utterplan-0.6.11}/PKG-INFO +12 -2
  4. {utterplan-0.6.8 → utterplan-0.6.11}/README.md +10 -0
  5. {utterplan-0.6.8 → utterplan-0.6.11}/docs/architecture.md +15 -1
  6. {utterplan-0.6.8 → utterplan-0.6.11}/docs/bundles.md +10 -0
  7. {utterplan-0.6.8 → utterplan-0.6.11}/docs/changelog.md +25 -1
  8. {utterplan-0.6.8 → utterplan-0.6.11}/docs/cli.md +3 -1
  9. {utterplan-0.6.8 → utterplan-0.6.11}/docs/debugging.md +13 -0
  10. {utterplan-0.6.8 → utterplan-0.6.11}/docs/python-api.md +6 -0
  11. {utterplan-0.6.8 → utterplan-0.6.11}/pyproject.toml +1 -1
  12. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_cli_bundle_render_book.py +51 -0
  13. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_cli_bundle_render_document.py +50 -0
  14. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_identity_units.py +61 -1
  15. utterplan-0.6.11/tests/test_prepared_semantics.py +188 -0
  16. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_renderplan_dependency_contract.py +1 -1
  17. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_renderplan_export.py +36 -0
  18. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_renderplan_v7_contract.py +73 -0
  19. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_replacement_provenance.py +23 -0
  20. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_ssmd_substitution_preparation.py +111 -0
  21. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/_book_service.py +7 -5
  22. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/_document_service.py +7 -5
  23. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/_version.py +3 -3
  24. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/bundle_replan.py +4 -2
  25. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/cli.py +13 -4
  26. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/flow_projection.py +73 -11
  27. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/planner.py +19 -1
  28. utterplan-0.6.11/utterplan/prepared_semantics.py +196 -0
  29. utterplan-0.6.11/utterplan/renderprofile_io.py +59 -0
  30. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan.egg-info/PKG-INFO +12 -2
  31. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan.egg-info/SOURCES.txt +3 -0
  32. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan.egg-info/requires.txt +1 -1
  33. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan.egg-info/scm_file_list.json +25 -0
  34. utterplan-0.6.11/utterplan.egg-info/scm_version.json +8 -0
  35. utterplan-0.6.8/utterplan.egg-info/scm_version.json +0 -8
  36. {utterplan-0.6.8 → utterplan-0.6.11}/.codecrate.toml +0 -0
  37. {utterplan-0.6.8 → utterplan-0.6.11}/.github/workflows/codecov.yml +0 -0
  38. {utterplan-0.6.8 → utterplan-0.6.11}/.github/workflows/pre-commit.yml +0 -0
  39. {utterplan-0.6.8 → utterplan-0.6.11}/.gitignore +0 -0
  40. {utterplan-0.6.8 → utterplan-0.6.11}/.pre-commit-config.yaml +0 -0
  41. {utterplan-0.6.8 → utterplan-0.6.11}/AGENTS.md +0 -0
  42. {utterplan-0.6.8 → utterplan-0.6.11}/LICENSE +0 -0
  43. {utterplan-0.6.8 → utterplan-0.6.11}/MANIFEST.in +0 -0
  44. {utterplan-0.6.8 → utterplan-0.6.11}/NOTICE +0 -0
  45. {utterplan-0.6.8 → utterplan-0.6.11}/docs/Makefile +0 -0
  46. {utterplan-0.6.8 → utterplan-0.6.11}/docs/conf.py +0 -0
  47. {utterplan-0.6.8 → utterplan-0.6.11}/docs/consumer-guide.md +0 -0
  48. {utterplan-0.6.8 → utterplan-0.6.11}/docs/coordinate-spaces.md +0 -0
  49. {utterplan-0.6.8 → utterplan-0.6.11}/docs/format.md +0 -0
  50. {utterplan-0.6.8 → utterplan-0.6.11}/docs/getting-started.md +0 -0
  51. {utterplan-0.6.8 → utterplan-0.6.11}/docs/index.md +0 -0
  52. {utterplan-0.6.8 → utterplan-0.6.11}/docs/make.bat +0 -0
  53. {utterplan-0.6.8 → utterplan-0.6.11}/docs/phase-a-completion.md +0 -0
  54. {utterplan-0.6.8 → utterplan-0.6.11}/docs/pykokoro-integration.md +0 -0
  55. {utterplan-0.6.8 → utterplan-0.6.11}/docs/requirements.txt +0 -0
  56. {utterplan-0.6.8 → utterplan-0.6.11}/examples/basic.py +0 -0
  57. {utterplan-0.6.8 → utterplan-0.6.11}/examples/chapter.ssmd +0 -0
  58. {utterplan-0.6.8 → utterplan-0.6.11}/examples/compile_file.py +0 -0
  59. {utterplan-0.6.8 → utterplan-0.6.11}/examples/inspect_plan.py +0 -0
  60. {utterplan-0.6.8 → utterplan-0.6.11}/examples/multilingual.py +0 -0
  61. {utterplan-0.6.8 → utterplan-0.6.11}/examples/ssmd_example.py +0 -0
  62. {utterplan-0.6.8 → utterplan-0.6.11}/setup.cfg +0 -0
  63. {utterplan-0.6.8 → utterplan-0.6.11}/spec/schemas/v1.schema.json +0 -0
  64. {utterplan-0.6.8 → utterplan-0.6.11}/spec/schemas/v2.schema.json +0 -0
  65. {utterplan-0.6.8 → utterplan-0.6.11}/spec/schemas/v3.schema.json +0 -0
  66. {utterplan-0.6.8 → utterplan-0.6.11}/spec/schemas/v4.schema.json +0 -0
  67. {utterplan-0.6.8 → utterplan-0.6.11}/spec/utterplan.schema.json +0 -0
  68. {utterplan-0.6.8 → utterplan-0.6.11}/tests/__init__.py +0 -0
  69. {utterplan-0.6.8 → utterplan-0.6.11}/tests/compiler_helpers.py +0 -0
  70. {utterplan-0.6.8 → utterplan-0.6.11}/tests/fixtures/canonical_consumer_contract.expected.toml +0 -0
  71. {utterplan-0.6.8 → utterplan-0.6.11}/tests/fixtures/canonical_consumer_contract.ssmd +0 -0
  72. {utterplan-0.6.8 → utterplan-0.6.11}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/document.ssmd.md +0 -0
  73. {utterplan-0.6.8 → utterplan-0.6.11}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/manifest.json +0 -0
  74. {utterplan-0.6.8 → utterplan-0.6.11}/tests/fixtures/ssmd_09_comprehensive.ssmd +0 -0
  75. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/basic_en.utterplan.toml +0 -0
  76. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/directives.utterplan.toml +0 -0
  77. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/markers.utterplan.toml +0 -0
  78. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/multilingual.utterplan.toml +0 -0
  79. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/parenthetical.utterplan.toml +0 -0
  80. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/spokenform_offsets.utterplan.toml +0 -0
  81. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/ssmd_09_comprehensive.utterplan.toml +0 -0
  82. {utterplan-0.6.8 → utterplan-0.6.11}/tests/golden/ssmd_breaks.utterplan.toml +0 -0
  83. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/__init__.py +0 -0
  84. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/cases.py +0 -0
  85. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/basic_en.utterplan.json +0 -0
  86. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/directives.utterplan.json +0 -0
  87. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/markers.utterplan.json +0 -0
  88. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/multilingual.utterplan.json +0 -0
  89. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/parenthetical.utterplan.json +0 -0
  90. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/spokenform_offsets.utterplan.json +0 -0
  91. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/ssmd_09_comprehensive.utterplan.json +0 -0
  92. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/fixtures/v4/ssmd_breaks.utterplan.json +0 -0
  93. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/goldens/multilingual_preparation.json +0 -0
  94. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/goldens/spokenform_numbers.json +0 -0
  95. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/goldens/ssmd_break.json +0 -0
  96. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/goldens/ssmd_language_detection.json +0 -0
  97. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/goldens/ssmd_marker.json +0 -0
  98. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/goldens/ssmd_pronunciation.json +0 -0
  99. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/test_legacy_json_import.py +0 -0
  100. {utterplan-0.6.8 → utterplan-0.6.11}/tests/migration/test_semantic_migration_regressions.py +0 -0
  101. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/README.md +0 -0
  102. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v1/basic.json +0 -0
  103. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v1/directives.json +0 -0
  104. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v1/markers.json +0 -0
  105. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v1/multilingual.json +0 -0
  106. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v1/parenthetical.json +0 -0
  107. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v1/spokenform_offsets.json +0 -0
  108. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v1/ssmd_breaks.json +0 -0
  109. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v2/directives.json +0 -0
  110. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v3/multilingual.json +0 -0
  111. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v3/ssmd_09_comprehensive.json +0 -0
  112. {utterplan-0.6.8 → utterplan-0.6.11}/tests/schema_history/v5/canonical_consumer_contract.toml +0 -0
  113. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_atomic_io.py +0 -0
  114. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_batch.py +0 -0
  115. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_boundaries.py +0 -0
  116. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_bundle_book_replan.py +0 -0
  117. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_bundle_book_services.py +0 -0
  118. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_bundle_document_compile.py +0 -0
  119. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_bundle_document_render.py +0 -0
  120. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_bundle_replan.py +0 -0
  121. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_bundle_role_inventory.py +0 -0
  122. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_bundle_services.py +0 -0
  123. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_cli.py +0 -0
  124. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_cli_batch.py +0 -0
  125. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_cli_bundle_compile_book.py +0 -0
  126. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_cli_bundle_document.py +0 -0
  127. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_cli_bundle_replan_book.py +0 -0
  128. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_compiler.py +0 -0
  129. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_consumer_contract.py +0 -0
  130. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_consumer_readiness.py +0 -0
  131. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_coordinates.py +0 -0
  132. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_dependency_architecture.py +0 -0
  133. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_directives.py +0 -0
  134. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_explain.py +0 -0
  135. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_fake_renderer.py +0 -0
  136. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_flow_compilation.py +0 -0
  137. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_format.py +0 -0
  138. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_golden.py +0 -0
  139. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_import_boundaries.py +0 -0
  140. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_language.py +0 -0
  141. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_linguistics.py +0 -0
  142. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_migration_framework.py +0 -0
  143. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_migration_v3_to_v4.py +0 -0
  144. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_migration_v4_to_v5.py +0 -0
  145. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_pauses.py +0 -0
  146. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_planner.py +0 -0
  147. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_preparation.py +0 -0
  148. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_preview.py +0 -0
  149. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_progress.py +0 -0
  150. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_public_identity.py +0 -0
  151. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_renderability.py +0 -0
  152. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_renderability_repair.py +0 -0
  153. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_renderplan_batch.py +0 -0
  154. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_renderplan_replan.py +0 -0
  155. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_role_coverage.py +0 -0
  156. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_schema_history.py +0 -0
  157. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_schema_registry.py +0 -0
  158. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_schema_strict.py +0 -0
  159. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_schema_v6_source.py +0 -0
  160. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_schema_v7.py +0 -0
  161. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_segment_topology.py +0 -0
  162. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_segmentation_adapter.py +0 -0
  163. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_segmentation_recovery.py +0 -0
  164. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_semantic_boundaries.py +0 -0
  165. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_sentence_topology.py +0 -0
  166. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_source_input.py +0 -0
  167. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_ssmd_09_parser.py +0 -0
  168. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_ssmd_bundle.py +0 -0
  169. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_ssmd_bundle_book.py +0 -0
  170. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_ssmd_bundle_document.py +0 -0
  171. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_token_topology.py +0 -0
  172. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_toml_format.py +0 -0
  173. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_ttsready_preparation_adapter.py +0 -0
  174. {utterplan-0.6.8 → utterplan-0.6.11}/tests/test_version.py +0 -0
  175. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/__init__.py +0 -0
  176. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/__main__.py +0 -0
  177. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/_book_replan.py +0 -0
  178. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/atomic_io.py +0 -0
  179. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/attempt_serialization.py +0 -0
  180. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/attempts.py +0 -0
  181. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/batch.py +0 -0
  182. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/boundaries.py +0 -0
  183. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/bundle.py +0 -0
  184. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/bundle_role_inventory.py +0 -0
  185. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/codecs/__init__.py +0 -0
  186. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/codecs/v4_toml.py +0 -0
  187. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/codecs/v5_toml.py +0 -0
  188. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/codecs/v6_toml.py +0 -0
  189. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/codecs/v7_toml.py +0 -0
  190. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/compiler.py +0 -0
  191. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/config.py +0 -0
  192. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/diagnostics.py +0 -0
  193. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/directives.py +0 -0
  194. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/exceptions.py +0 -0
  195. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/explain.py +0 -0
  196. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/format.py +0 -0
  197. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/hashing.py +0 -0
  198. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/language.py +0 -0
  199. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/linguistics.py +0 -0
  200. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migration.py +0 -0
  201. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/__init__.py +0 -0
  202. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/registry.py +0 -0
  203. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/v1_to_v2.py +0 -0
  204. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/v2_to_v3.py +0 -0
  205. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/v3_to_v4.py +0 -0
  206. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/v4_to_v5.py +0 -0
  207. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/v5_to_v6.py +0 -0
  208. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/migrations/v6_to_v7.py +0 -0
  209. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/model.py +0 -0
  210. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/parsers.py +0 -0
  211. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/pauses.py +0 -0
  212. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/preparation.py +0 -0
  213. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/preview.py +0 -0
  214. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/progress.py +0 -0
  215. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/py.typed +0 -0
  216. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/renderability.py +0 -0
  217. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/renderplan_batch.py +0 -0
  218. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/renderplan_export.py +0 -0
  219. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/renderplan_grouping.py +0 -0
  220. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/renderplan_projection.py +0 -0
  221. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/renderplan_replan.py +0 -0
  222. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/renderplan_workspace.py +0 -0
  223. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/role_coverage.py +0 -0
  224. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/schema_registry.py +0 -0
  225. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/schemas/v1.schema.json +0 -0
  226. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/schemas/v2.schema.json +0 -0
  227. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/schemas/v3.schema.json +0 -0
  228. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/schemas/v4.schema.json +0 -0
  229. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/segmentation.py +0 -0
  230. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/serialization.py +0 -0
  231. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/source_input.py +0 -0
  232. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/spacy_models.py +0 -0
  233. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/spans.py +0 -0
  234. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/ssmd_bundle.py +0 -0
  235. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/__init__.py +0 -0
  236. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/document/__init__.py +0 -0
  237. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/document/plain.py +0 -0
  238. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/document/ssmd.py +0 -0
  239. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/preparation/__init__.py +0 -0
  240. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/preparation/identity.py +0 -0
  241. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/preparation/ssmd.py +0 -0
  242. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/preparation/ttsready.py +0 -0
  243. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/stages/protocols.py +0 -0
  244. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/token_topology.py +0 -0
  245. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/toml_codec.py +0 -0
  246. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/trace_codec.py +0 -0
  247. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/units.py +0 -0
  248. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/utterplan.schema.json +0 -0
  249. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/versioning.py +0 -0
  250. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan/workspace_paths.py +0 -0
  251. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan.egg-info/dependency_links.txt +0 -0
  252. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan.egg-info/entry_points.txt +0 -0
  253. {utterplan-0.6.8 → utterplan-0.6.11}/utterplan.egg-info/top_level.txt +0 -0
@@ -82,7 +82,7 @@ jobs:
82
82
  assert has_range(requirements, "phrasplit", "0.3.9", "0.4")
83
83
  assert has_range(requirements, "ssmd", "0.9.3", "0.10")
84
84
  assert has_range(requirements, "ttsready", "0.2.3", "0.3")
85
- assert has_range(requirements, "renderplan", "0.1.1", "0.2")
85
+ assert has_range(requirements, "renderplan", "0.1.4", "0.2")
86
86
  names = set(archive.namelist())
87
87
  expected = {
88
88
  "utterplan/py.typed",
@@ -120,7 +120,7 @@ jobs:
120
120
  "phrasplit": ("0.3.9", "0.4"),
121
121
  "ssmd": ("0.9.3", "0.10"),
122
122
  "ttsready": ("0.2.3", "0.3"),
123
- "renderplan": ("0.1.1", "0.2"),
123
+ "renderplan": ("0.1.4", "0.2"),
124
124
  }
125
125
 
126
126
  def check_metadata(contents):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utterplan
3
- Version: 0.6.8
3
+ Version: 0.6.11
4
4
  Summary: Engine-independent text-to-speech planning compiler and interchange format
5
5
  Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
6
6
  License: Apache License
@@ -227,7 +227,7 @@ Requires-Dist: ssmd<0.10,>=0.9.3
227
227
  Requires-Dist: ttsready<0.3,>=0.2.3
228
228
  Requires-Dist: typing_extensions>=4.0
229
229
  Requires-Dist: tomlkit<1,>=0.13
230
- Requires-Dist: renderplan<0.2,>=0.1.1
230
+ Requires-Dist: renderplan<0.2,>=0.1.4
231
231
  Provides-Extra: dev
232
232
  Requires-Dist: build; extra == "dev"
233
233
  Requires-Dist: jsonschema; extra == "dev"
@@ -408,6 +408,16 @@ engine-backed profiles, then preflights and renders the resulting RenderPlans.
408
408
  UtterPlan's single-file `render-plan` command remains the low-level projection and
409
409
  deterministic replan primitive.
410
410
 
411
+ ### Troubleshooting RenderProfile identity mismatch
412
+
413
+ A RenderProfile `profile_id` is a semantic content identity validated by RenderPlan; it is not a hash of the TOML file bytes. If the profile's identity-bearing semantics change without refreshing its ID, UtterPlan rejects it before creating RenderPlan artifacts. Regenerate or re-save the complete profile with VoiceRender, then validate it before rendering:
414
+
415
+ ```bash
416
+ voicerender profile validate narration.renderprofile.toml
417
+ ```
418
+
419
+ Do not replace `profile_id` with `sha256sum` of the TOML file. `--force` only replaces an otherwise valid existing RenderPlan output; it never bypasses profile validation.
420
+
411
421
  When VoiceRender reports `preflight.replan`, refine the active bundle variant
412
422
  using matching feedback:
413
423
 
@@ -164,6 +164,16 @@ engine-backed profiles, then preflights and renders the resulting RenderPlans.
164
164
  UtterPlan's single-file `render-plan` command remains the low-level projection and
165
165
  deterministic replan primitive.
166
166
 
167
+ ### Troubleshooting RenderProfile identity mismatch
168
+
169
+ A RenderProfile `profile_id` is a semantic content identity validated by RenderPlan; it is not a hash of the TOML file bytes. If the profile's identity-bearing semantics change without refreshing its ID, UtterPlan rejects it before creating RenderPlan artifacts. Regenerate or re-save the complete profile with VoiceRender, then validate it before rendering:
170
+
171
+ ```bash
172
+ voicerender profile validate narration.renderprofile.toml
173
+ ```
174
+
175
+ Do not replace `profile_id` with `sha256sum` of the TOML file. `--force` only replaces an otherwise valid existing RenderPlan output; it never bypasses profile validation.
176
+
167
177
  When VoiceRender reports `preflight.replan`, refine the active bundle variant
168
178
  using matching feedback:
169
179
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Architecture Documentation"
3
- version: 4
3
+ version: 6
4
4
  generator: "archledger 0.4.0"
5
5
  arc42_template_version: "9.0-EN"
6
6
  ---
@@ -237,6 +237,14 @@ Supporting tactics:
237
237
  evidence live in an optional versioned trace TOML that never affects plan
238
238
  identity or executable content.
239
239
 
240
+ ## Prepared character-spelling semantics
241
+
242
+ After written-to-spoken preparation and before segmentation, UtterPlan inspects verified replacement records for the exact character-spelling transformation: an ASCII uppercase-letter/digit core of at least two characters becomes the same characters joined by single spaces, optionally retaining an identical `'s` or `’s` suffix. This derives a compiler-owned `say_as=characters` annotation over only the prepared character core. For example, `[ART]{sub=\"A R T\"}` keeps prepared text `A R T`; `[ART's]{sub=\"A R T's\"}` keeps `A R T's` and marks only `A R T`.
243
+
244
+ The derivation is portable executable intent, not a renderer-specific spelling rewrite. Prepared text never becomes `A- R- T`; a renderer adapter may choose that frontend form. Existing semantic-edge segmentation produces the exact controlled span, while RenderPlan sentence grouping retains the full sentence as one normal speech request with a request-local control. This feature adds neither a sentence split nor an automatic pause.
245
+
246
+ Authored `say-as` semantics take precedence, and the consumed SSMD `sub` annotation remains consumed. The derived directive uses existing schema-v7 `SayAsDirective` serialization and identity hashing; replacement records remain diagnostic provenance and are not synthesis input. No schema version or migration changes are required.
247
+
240
248
  ## Strategy Items
241
249
 
242
250
  <!-- archledger: no accepted records for this section yet -->
@@ -281,6 +289,8 @@ from `manifest.json`, normalizes item selection, and enforces path containment.
281
289
  services; private book and document helpers retain kind-specific persistence and
282
290
  report behavior. The generic source loader remains limited to standalone files.
283
291
 
292
+ Preparation semantics include the compiler-only `prepared_semantics.py` pass. It consumes verified preparation replacement records after text preparation and before language-run/segment construction, appending deterministic `say-as=characters` annotations without changing prepared text, replacement provenance, boundaries, source maps, or consumed substitutions. It reuses existing directive resolution, semantic-edge segmentation, FlowPlan v7 serialization/identity, and RenderPlan request-local control projection.
293
+
284
294
  <!-- archledger: no accepted records for this section yet -->
285
295
 
286
296
  # Runtime View
@@ -479,6 +489,10 @@ semantic blockers. Blocked planning outcomes may be reviewed through the separat
479
489
  `utterplan.planning-attempt.v1` inspect-only artifact; such candidates remain
480
490
  drafts and never become canonical plans.
481
491
 
492
+ ### Character spelling: text, intent, and realization
493
+
494
+ Character spelling keeps four concerns separate: source spelling (for example, `ART`), prepared spoken text (`A R T`), portable executable intent (`say_as=characters` over the exact prepared core), and downstream renderer realization. The matcher uses only verified replacement metadata and does not guess from arbitrary uppercase words or normalization changes. Possessive suffixes remain outside the semantic range. Replacement provenance is excluded from executable identity; the derived directive affects FlowUnit and FlowPlan identity. UtterPlan never persists a frontend-specific hyphen form or adds a pause for this feature.
495
+
482
496
  <!-- archledger: no accepted records for this section yet -->
483
497
 
484
498
  # Architecture Decisions
@@ -77,6 +77,16 @@ The default RenderPlan root is `<bundle>/renderplan/`. Each variant includes a c
77
77
 
78
78
  Role coverage is checked before new variant artifacts are written. The selected profile must cover every required logical role, including its default role when any speech has no explicit role. Profile identity mismatches, unsafe paths, and metadata collisions are not bypassed by `--force`.
79
79
 
80
+ ### Troubleshooting stale RenderProfile identity
81
+
82
+ RenderProfile IDs represent semantic profile content, not the bytes or formatting of the TOML file. A profile edited after its identity was persisted (or produced with a stale identity) fails validation before UtterPlan creates a variant. Regenerate or re-save the complete profile with VoiceRender and validate it before rendering:
83
+
84
+ ```bash
85
+ voicerender profile validate narration.renderprofile.toml
86
+ ```
87
+
88
+ Do not set `profile_id` to the result of `sha256sum` on the TOML file; RenderPlan owns the canonical semantic identity. `--force` permits replacing an otherwise valid existing RenderPlan output only. It does not bypass profile identity/schema checks, role coverage, unsafe paths, or snapshot mismatches.
89
+
80
90
  ## Replan
81
91
 
82
92
  When VoiceRender reports `preflight.replan`, refine the active plan using its exact matching feedback:
@@ -4,7 +4,31 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ## [v0.6.8] - Unreleased
7
+ ## [v0.6.10] - 2026-10-28
8
+
9
+ ### Added
10
+
11
+ - Added portable character-spelling semantics for verified substitutions
12
+
13
+ <!-- generated by releaseledger -->
14
+
15
+ ## [v0.6.9] - 2026-10-27
16
+
17
+ ### Added
18
+
19
+ - Added shared RenderProfile loading with canonical path resolution, validation, and actionable stale-identity diagnostics
20
+
21
+ ### Changed
22
+
23
+ - Changed bundle rendering and replanning to use validated profile identities and keep --force limited to valid outputs
24
+
25
+ ### Fixed
26
+
27
+ - Fixed token projection to preserve optional morphology and report missing required fields with locators
28
+
29
+ <!-- generated by releaseledger -->
30
+
31
+ ## [v0.6.8] - 2026-10-23
8
32
 
9
33
  ### Added
10
34
 
@@ -161,7 +161,9 @@ utterplan bundle replan BUNDLE --variant VARIANT [--chapters SELECTOR]
161
161
 
162
162
  Compilation writes one schema-v7 FlowPlan per selected item and an operational `compile-report.toml` under `<bundle>/utterplan/` by default. Rendering reads those saved plans and requires an explicit RenderProfile; it writes profile-fingerprinted RenderPlans, a profile snapshot, and an operational report (book bundles also maintain a set index) under `<bundle>/renderplan/`. Replanning refines active plans from matching VoiceRender preflight feedback and updates only the selected active entries. These commands never mutate the manifest or source documents. `render-plan` remains the low-level one-file projection/replan command.
163
163
 
164
- The diagnostics option controls compile terminal verbosity; failures are always reported and complete diagnostics remain in the operational report. `--fail-fast` skips later selected items after a failure. `--force` replaces selected plan artifacts but does not bypass unsafe paths, metadata collisions, or profile-identity checks. Exit status is 0 when all selected work succeeds, 1 when an item fails, and 2 for usage or shared-configuration errors. See the [bundle guide](bundles.md) for layouts, safety behavior, and Python examples.
164
+ The diagnostics option controls compile terminal verbosity; failures are always reported and complete diagnostics remain in the operational report. `--fail-fast` skips later selected items after a failure. For compile commands, `--force` replaces selected plan artifacts. For rendering, it only replaces an otherwise valid existing RenderPlan output; it does not bypass unsafe paths, metadata collisions, role-coverage failures, profile schema/identity validation, or existing snapshot identity checks. Exit status is 0 when all selected work succeeds, 1 when an item fails, and 2 for usage or shared-configuration errors. See the [bundle guide](bundles.md) for layouts, safety behavior, and Python examples.
165
+
166
+ A RenderProfile `profile_id` is a semantic content identity, not a hash of TOML bytes. If it is stale, the path-aware error recommends regenerating or re-saving the profile with VoiceRender and validating it with `voicerender profile validate PROFILE` before rendering. Do not substitute `sha256sum` of the profile file; `--force` never bypasses this validation.
165
167
 
166
168
  Explain an existing compiled plan as a human-readable speech narrative:
167
169
 
@@ -40,3 +40,16 @@ If spoken wording is wrong, inspect `FlowSegment.text`. If effective language is
40
40
  For source-coordinate bugs, inspect `PreparationTrace.source_text`, `source_spans`, and the structural/spoken maps. Source offsets address exact input Python characters, including SSMD headers and markup; diagnostic line and column are 1-based. Preparation transformation source ranges address structural text, while output ranges address prepared spoken text. See the [coordinate-space contract](coordinate-spaces).
41
41
 
42
42
  If the FlowPlan is correct but G2P or audio is wrong, the issue belongs to the renderer or acoustic runtime, not the planning boundary.
43
+
44
+ ## RenderProfile identity mismatch
45
+
46
+ A RenderProfile's `profile_id` identifies its canonical semantic content, not its TOML serialization. The error `profile_id does not match profile content` means the persisted ID is stale for the current profile semantics. UtterPlan fails closed and names the profile path; it does not repair or re-hash the profile, and `--force` does not bypass validation.
47
+
48
+ Regenerate or re-save the complete engine-backed RenderProfile with VoiceRender, then validate it before invoking UtterPlan:
49
+
50
+ ```bash
51
+ voicerender profile validate narration.renderprofile.toml
52
+ utterplan bundle render story.ssmd-bundle --profile narration.renderprofile.toml --force
53
+ ```
54
+
55
+ Do not use `sha256sum` on the TOML file as a replacement ID. If VoiceRender created the file and it was not manually edited, report the producer path for investigation; all binding and renderer metadata must be assembled before the final profile identity is persisted.
@@ -329,6 +329,12 @@ print(segment.directives.emphasis)
329
329
 
330
330
  The `document` record retains compact portable document information. Effective directives and pause intents live on their owning flow segments. Compiler-only source spans and preparation coordinate maps remain in the optional trace. See the [coordinate-space contract](coordinate-spaces) and [consumer guide](consumer-guide) for renderer responsibilities.
331
331
 
332
+ ### Character-spelling substitutions
333
+
334
+ When a verified preparation replacement is exactly an ASCII uppercase-letter/digit sequence expanded into its individual characters separated by single spaces, UtterPlan derives `say_as=characters` for that prepared core. For example, `[ART]{sub="A R T"}` keeps prepared text `A R T`; `[ART's]{sub="A R T's"}` keeps `A R T's` and covers only `A R T`. This applies to exact SSMD substitutions and other preparation backends' verified replacement records, including TTSReady. It does not guess from arbitrary acronyms, case changes, normalization, punctuation, or altered spacing. An authored `say-as` takes precedence.
335
+
336
+ The derived directive is executable semantic intent and participates in FlowPlan identity. Replacement provenance remains diagnostic only. With sentence-level RenderPlan grouping, the full sentence stays one speech request and the request-local `say_as` control covers only the spelled core. UtterPlan never rewrites prepared text to frontend-specific forms such as `A- R- T` and does not add an automatic pause for this feature; an engine adapter owns any renderer-specific realization.
337
+
332
338
  ## Flow segments and local tokens
333
339
 
334
340
  A `FlowPlan` stores ordered `FlowUnit` objects. Each unit owns its ordered segments, so consumers do not join plan-global IDs. Token offsets use Python-character coordinates local to `segment.text`:
@@ -17,7 +17,7 @@ dependencies = [
17
17
  "ttsready>=0.2.3,<0.3",
18
18
  "typing_extensions>=4.0",
19
19
  "tomlkit>=0.13,<1",
20
- "renderplan>=0.1.1,<0.2",
20
+ "renderplan>=0.1.4,<0.2",
21
21
  ]
22
22
  classifiers = [
23
23
  "Development Status :: 3 - Alpha",
@@ -183,6 +183,7 @@ def test_render_book_success_matches_single_file_projection(tmp_path: Path, caps
183
183
  root = _write_book(tmp_path / "ordered.ssmd-bundle", (5, 7, 6))
184
184
  profile_path = root / "narration.renderprofile.toml"
185
185
  profile = _profile(profile_path)
186
+ profile_bytes_before = profile_path.read_bytes()
186
187
  manifest_before = (root / "manifest.json").read_bytes()
187
188
 
188
189
  result = _render(root, profile_path, "--chapters", "5-7")
@@ -221,6 +222,7 @@ def test_render_book_success_matches_single_file_projection(tmp_path: Path, caps
221
222
  assert report["failed"] == 0
222
223
  assert len(report["item"]) == 3
223
224
  assert (root / "manifest.json").read_bytes() == manifest_before
225
+ assert profile_path.read_bytes() == profile_bytes_before
224
226
 
225
227
  source_plan = root / "utterplan/chapter-0005.utterplan.toml"
226
228
  single_output = tmp_path / "chapter-0005.single.renderplan.toml"
@@ -243,6 +245,55 @@ def test_render_book_success_matches_single_file_projection(tmp_path: Path, caps
243
245
  assert batch_plan.plan_id == single_plan.plan_id
244
246
 
245
247
 
248
+ @pytest.mark.parametrize("force", (False, True))
249
+ def test_stale_profile_identity_fails_closed_with_and_without_force(
250
+ tmp_path: Path, capsys: pytest.CaptureFixture[str], force: bool
251
+ ) -> None:
252
+ root = _write_book(tmp_path / "stale.ssmd-bundle", (5,))
253
+ profile_path = root / "stale.renderprofile.toml"
254
+ _profile(profile_path)
255
+ stale_content = profile_path.read_text(encoding="utf-8").replace(
256
+ 'voice = "amy"', 'voice = "changed"', 1
257
+ )
258
+ assert stale_content != profile_path.read_text(encoding="utf-8")
259
+ profile_path.write_text(stale_content, encoding="utf-8")
260
+
261
+ options = ("--force",) if force else ()
262
+ assert _render(root, profile_path, *options) != 0
263
+ error = capsys.readouterr().err
264
+ assert f"invalid RenderProfile {profile_path.resolve()}" in error
265
+ assert "profile_id does not match profile content" in error
266
+ assert "stale for the current profile semantics" in error
267
+ assert "Regenerate or re-save the RenderProfile with its producer" in error
268
+ assert "validate it before rendering" in error
269
+ assert "--force does not bypass profile identity validation" in error
270
+ assert not (root / "renderplan").exists()
271
+
272
+
273
+ @pytest.mark.parametrize("invalid_kind", ("missing", "malformed", "schema"))
274
+ def test_profile_load_failures_include_path_and_write_nothing(
275
+ tmp_path: Path, capsys, invalid_kind: str
276
+ ) -> None:
277
+ root = _write_book(tmp_path / f"{invalid_kind}.ssmd-bundle", (5,))
278
+ profile_path = root / f"{invalid_kind}.renderprofile.toml"
279
+ if invalid_kind == "malformed":
280
+ profile_path.write_text("[roles\n", encoding="utf-8")
281
+ elif invalid_kind == "schema":
282
+ _profile(profile_path)
283
+ profile_path.write_text(
284
+ profile_path.read_text(encoding="utf-8").replace(
285
+ "schema_version = 1", "schema_version = 99"
286
+ ),
287
+ encoding="utf-8",
288
+ )
289
+
290
+ assert _render(root, profile_path) != 0
291
+ error = capsys.readouterr().err
292
+ assert "invalid RenderProfile" in error
293
+ assert str(profile_path.resolve()) in error
294
+ assert not (root / "renderplan").exists()
295
+
296
+
246
297
  def test_render_book_reports_multi_role_coverage(tmp_path: Path, capsys) -> None:
247
298
  root = _write_book(tmp_path / "multi-role.ssmd-bundle", (5, 6, 7))
248
299
  _write_role_plan(root, 5, "host")
@@ -110,6 +110,7 @@ def test_render_document_writes_profile_specific_direct_variant_and_report(
110
110
  source_plan_before = source_plan.read_bytes()
111
111
  profile_path = tmp_path / "narration.renderprofile.toml"
112
112
  profile = _profile(profile_path)
113
+ profile_bytes_before = profile_path.read_bytes()
113
114
 
114
115
  assert _render(root, profile_path) == 0
115
116
  capsys.readouterr()
@@ -146,6 +147,55 @@ def test_render_document_writes_profile_specific_direct_variant_and_report(
146
147
  "utterplan",
147
148
  "renderplan",
148
149
  }
150
+ assert profile_path.read_bytes() == profile_bytes_before
151
+
152
+
153
+ def test_stale_profile_identity_fails_closed_with_and_without_force(
154
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
155
+ ) -> None:
156
+ root = _workspace(tmp_path)
157
+ profile_path = tmp_path / "stale.renderprofile.toml"
158
+ _profile(profile_path)
159
+ stale_content = profile_path.read_text(encoding="utf-8").replace(
160
+ 'voice = "amy"', 'voice = "changed"', 1
161
+ )
162
+ assert stale_content != profile_path.read_text(encoding="utf-8")
163
+ profile_path.write_text(stale_content, encoding="utf-8")
164
+
165
+ for options in ((), ("--force",)):
166
+ assert _render(root, profile_path, *options) != 0
167
+ error = capsys.readouterr().err
168
+ assert f"invalid RenderProfile {profile_path.resolve()}" in error
169
+ assert "profile_id does not match profile content" in error
170
+ assert "stale for the current profile semantics" in error
171
+ assert "Regenerate or re-save the RenderProfile with its producer" in error
172
+ assert "validate it before rendering" in error
173
+ assert "--force does not bypass profile identity validation" in error
174
+ assert not (root / "renderplan").exists()
175
+
176
+
177
+ @pytest.mark.parametrize("invalid_kind", ("missing", "malformed", "schema"))
178
+ def test_profile_load_failures_include_path_and_write_nothing(
179
+ tmp_path: Path, capsys: pytest.CaptureFixture[str], invalid_kind: str
180
+ ) -> None:
181
+ root = _workspace(tmp_path)
182
+ profile_path = tmp_path / f"{invalid_kind}.renderprofile.toml"
183
+ if invalid_kind == "malformed":
184
+ profile_path.write_text("[roles\n", encoding="utf-8")
185
+ elif invalid_kind == "schema":
186
+ _profile(profile_path)
187
+ profile_path.write_text(
188
+ profile_path.read_text(encoding="utf-8").replace(
189
+ "schema_version = 1", "schema_version = 99"
190
+ ),
191
+ encoding="utf-8",
192
+ )
193
+
194
+ assert _render(root, profile_path) != 0
195
+ error = capsys.readouterr().err
196
+ assert "invalid RenderProfile" in error
197
+ assert str(profile_path.resolve()) in error
198
+ assert not (root / "renderplan").exists()
149
199
 
150
200
 
151
201
  def test_missing_utterplan_fails_actionably_without_partial_variant(
@@ -2,7 +2,7 @@ from dataclasses import replace
2
2
  from types import SimpleNamespace
3
3
 
4
4
  from tests.compiler_helpers import CompilerTestPlanner as UtterancePlanner
5
- from utterplan import PlannerConfig, SemanticBoundary
5
+ from utterplan import FlowUnit, LinguisticsConfig, PlannerConfig, SemanticBoundary, compile_document
6
6
  from utterplan.hashing import (
7
7
  LEGACY_UNIT_HASH_SCHEMA,
8
8
  UNIT_HASH_SCHEMA,
@@ -182,3 +182,63 @@ def test_serialized_and_runtime_unit_hash_payloads_match() -> None:
182
182
  migrated_payload = unit_hash_payload_from_serialized(serialized_unit, serialized_plan)
183
183
 
184
184
  assert migrated_payload == runtime_payload
185
+
186
+
187
+ def test_derived_say_as_changes_identity_and_v7_roundtrips_without_hashing_provenance() -> None:
188
+ source = 'Because someone called [ART]{sub="A R T"} finally.'
189
+ plan = compile_document(
190
+ source,
191
+ input_format="ssmd",
192
+ config=PlannerConfig(
193
+ language="en-us",
194
+ document_format="ssmd",
195
+ linguistics=LinguisticsConfig(use_spacy=False),
196
+ ),
197
+ ).plan
198
+ assert "".join(segment.text for unit in plan.flow for segment in unit.segments) == (
199
+ "Because someone called A R T finally."
200
+ )
201
+ spelled_ref = next(
202
+ (unit_index, segment_index, segment)
203
+ for unit_index, unit in enumerate(plan.flow)
204
+ for segment_index, segment in enumerate(unit.segments)
205
+ if segment.text == "A R T"
206
+ )
207
+ unit_index, segment_index, spelled = spelled_ref
208
+ assert spelled.directives.say_as is not None
209
+ assert spelled.directives.say_as.interpret_as == "characters"
210
+
211
+ without_units = []
212
+ for unit in plan.flow:
213
+ without_units.append(
214
+ FlowUnit(
215
+ tuple(
216
+ replace(
217
+ segment,
218
+ directives=replace(segment.directives, say_as=None),
219
+ )
220
+ for segment in unit.segments
221
+ )
222
+ )
223
+ )
224
+ without_say_as = replace(plan, flow=tuple(without_units), plan_id="")
225
+ assert without_say_as.flow[unit_index].segments[segment_index].text == spelled.text
226
+ assert without_say_as.flow[unit_index].content_hash != plan.flow[unit_index].content_hash
227
+ assert without_say_as.plan_id != plan.plan_id
228
+
229
+ replacement = spelled.replacements[0]
230
+ provenance_only = replace(replacement, source="different diagnostic spelling")
231
+ changed_segments = list(plan.flow[unit_index].segments)
232
+ changed_segments[segment_index] = replace(spelled, replacements=(provenance_only,))
233
+ changed_units = list(plan.flow)
234
+ changed_units[unit_index] = FlowUnit(tuple(changed_segments))
235
+ provenance_changed = replace(plan, flow=tuple(changed_units), plan_id="")
236
+ assert provenance_changed.flow[unit_index].content_hash == plan.flow[unit_index].content_hash
237
+ assert provenance_changed.plan_id == plan.plan_id
238
+
239
+ restored = type(plan).from_toml(plan.to_toml())
240
+ assert restored == plan
241
+ assert restored.schema_version == 7
242
+ assert restored.flow[unit_index].segments[segment_index].directives.say_as == (
243
+ spelled.directives.say_as
244
+ )
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ import pytest
7
+
8
+ from utterplan.model import AnnotationSpan, BoundaryEvent, TextPreparationInfo
9
+ from utterplan.preparation import PreparedText
10
+ from utterplan.prepared_semantics import (
11
+ CharacterSpellingMatch,
12
+ detect_character_spelling,
13
+ infer_prepared_semantics,
14
+ )
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class _MappedSource:
19
+ source_length: int
20
+ output_length: int
21
+ source_start: int
22
+ source_end: int
23
+ output_start: int
24
+ output_end: int
25
+
26
+ def map_source_span(self, start: int, end: int) -> tuple[int, int]:
27
+ if (start, end) == (self.source_start, self.source_end):
28
+ return self.output_start, self.output_end
29
+ return start, end
30
+
31
+ def map_output_span(self, start: int, end: int) -> tuple[int, int]:
32
+ if (start, end) == (self.output_start, self.output_end):
33
+ return self.source_start, self.source_end
34
+ return start, end
35
+
36
+
37
+ def _replacement(source: str, replacement: str, *, start: int = 0) -> PreparedText:
38
+ output_start = start + 10
39
+ source_map = _MappedSource(
40
+ source_length=start + len(source) + 5,
41
+ output_length=output_start + len(replacement) + 5,
42
+ source_start=start,
43
+ source_end=start + len(source),
44
+ output_start=output_start,
45
+ output_end=output_start + len(replacement),
46
+ )
47
+ spoken = " " * output_start + replacement + " " * 5
48
+ record: dict[str, Any] = {
49
+ "source_start": start,
50
+ "source_end": start + len(source),
51
+ "output_start": output_start,
52
+ "output_end": output_start + len(replacement),
53
+ "source": source,
54
+ "replacement": replacement,
55
+ "kind": "ssmd-sub",
56
+ "rule": "ssmd.sub",
57
+ "annotation_id": "sub-1",
58
+ }
59
+ source_annotation = AnnotationSpan(
60
+ id="sub-1",
61
+ kind="directive",
62
+ attrs={"sub": replacement},
63
+ structural_start=start,
64
+ structural_end=start + len(source),
65
+ spoken_start=output_start,
66
+ spoken_end=output_start + len(replacement),
67
+ source_start=100 + start,
68
+ source_end=100 + start + len(source),
69
+ source_node_id="source-node-1",
70
+ )
71
+ return PreparedText(
72
+ spoken_text=spoken,
73
+ info=TextPreparationInfo("ssmd", None, replacements=(record,)),
74
+ annotations=(source_annotation,),
75
+ boundaries=(BoundaryEvent("b-1", 0, "sentence"),),
76
+ source_map=source_map,
77
+ consumed_substitutions=("sub-1",),
78
+ )
79
+
80
+
81
+ @pytest.mark.parametrize(
82
+ ("source", "replacement", "expected"),
83
+ [
84
+ ("ART", "A R T", CharacterSpellingMatch(0, 3, 0, 5)),
85
+ ("AI", "A I", CharacterSpellingMatch(0, 2, 0, 3)),
86
+ ("USA", "U S A", CharacterSpellingMatch(0, 3, 0, 5)),
87
+ ("R2D2", "R 2 D 2", CharacterSpellingMatch(0, 4, 0, 7)),
88
+ ("ART's", "A R T's", CharacterSpellingMatch(0, 3, 0, 5)),
89
+ ("ART’s", "A R T’s", CharacterSpellingMatch(0, 3, 0, 5)),
90
+ ],
91
+ )
92
+ def test_detect_exact_character_spelling(
93
+ source: str, replacement: str, expected: CharacterSpellingMatch
94
+ ) -> None:
95
+ assert detect_character_spelling(source, replacement) == expected
96
+
97
+
98
+ @pytest.mark.parametrize(
99
+ ("source", "replacement"),
100
+ [
101
+ ("A", "A"),
102
+ ("NASA", "nasa"),
103
+ ("NASA", "N A S A agency"),
104
+ ("Dr.", "Doctor"),
105
+ ("kg", "kilograms"),
106
+ ("ART", "A-R-T"),
107
+ ("ABC", "A B C D"),
108
+ ("AB", "A B"),
109
+ ("A&B", "A and B"),
110
+ ("ARTs", "A R T s"),
111
+ ("ART's", "A R T’s"),
112
+ ],
113
+ )
114
+ def test_rejects_non_exact_character_spelling(source: str, replacement: str) -> None:
115
+ assert detect_character_spelling(source, replacement) is None
116
+
117
+
118
+ def test_inference_adds_immutable_core_only_deterministic_annotation() -> None:
119
+ prepared = _replacement("ART's", "A R T's", start=4)
120
+ result = infer_prepared_semantics(prepared)
121
+ derived = result.annotations[-1]
122
+
123
+ assert result.spoken_text == prepared.spoken_text
124
+ assert result.info is prepared.info
125
+ assert result.info.replacements == prepared.info.replacements
126
+ assert result.boundaries is prepared.boundaries
127
+ assert result.source_map is prepared.source_map
128
+ assert result.consumed_substitutions is prepared.consumed_substitutions
129
+ assert derived.id == "derived.say-as.characters.4.7.14.19"
130
+ assert derived.kind == "directive"
131
+ assert derived.attrs == {
132
+ "tag": "say-as",
133
+ "as": "characters",
134
+ "origin": "derived-character-spelling",
135
+ }
136
+ assert (derived.structural_start, derived.structural_end) == (4, 7)
137
+ assert (derived.spoken_start, derived.spoken_end) == (14, 19)
138
+ assert prepared.spoken_text[derived.spoken_start : derived.spoken_end] == "A R T"
139
+ assert (derived.source_start, derived.source_end) == (104, 109)
140
+ assert derived.source_node_id == "source-node-1"
141
+ assert infer_prepared_semantics(result).annotations == result.annotations
142
+
143
+
144
+ def test_authored_say_as_prevents_derived_overlap_and_wins() -> None:
145
+ prepared = _replacement("ART", "A R T")
146
+ authored = AnnotationSpan(
147
+ id="authored-say-as",
148
+ kind="directive",
149
+ attrs={"tag": "say-as", "as": "initialism"},
150
+ structural_start=0,
151
+ structural_end=3,
152
+ spoken_start=10,
153
+ spoken_end=15,
154
+ )
155
+ prepared = PreparedText(
156
+ prepared.spoken_text,
157
+ prepared.info,
158
+ (*prepared.annotations, authored),
159
+ prepared.boundaries,
160
+ prepared.source_map,
161
+ prepared.consumed_substitutions,
162
+ )
163
+
164
+ result = infer_prepared_semantics(prepared)
165
+
166
+ assert result is prepared
167
+ assert [item.id for item in result.annotations].count("authored-say-as") == 1
168
+ assert all(
169
+ item.attrs.get("origin") != "derived-character-spelling" for item in result.annotations
170
+ )
171
+
172
+
173
+ def test_unverified_replacement_is_ignored() -> None:
174
+ prepared = _replacement("ART", "A R T")
175
+ bad = dict(
176
+ prepared.info.replacements[0], output_end=prepared.info.replacements[0]["output_end"] - 1
177
+ )
178
+ info = TextPreparationInfo("ssmd", None, replacements=(bad,))
179
+ unverified = PreparedText(
180
+ prepared.spoken_text,
181
+ info,
182
+ prepared.annotations,
183
+ prepared.boundaries,
184
+ prepared.source_map,
185
+ prepared.consumed_substitutions,
186
+ )
187
+
188
+ assert infer_prepared_semantics(unverified) is unverified
@@ -29,4 +29,4 @@ def test_renderplan_collection_api_minimum_is_declared_as_runtime_dependency() -
29
29
  project = tomlkit.parse(
30
30
  (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text(encoding="utf-8")
31
31
  ).unwrap()
32
- assert "renderplan>=0.1.1,<0.2" in project["project"]["dependencies"]
32
+ assert "renderplan>=0.1.4,<0.2" in project["project"]["dependencies"]