utterplan 0.6.8__tar.gz → 0.6.9__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 (251) hide show
  1. {utterplan-0.6.8 → utterplan-0.6.9}/PKG-INFO +11 -1
  2. {utterplan-0.6.8 → utterplan-0.6.9}/README.md +10 -0
  3. {utterplan-0.6.8 → utterplan-0.6.9}/docs/bundles.md +10 -0
  4. {utterplan-0.6.8 → utterplan-0.6.9}/docs/changelog.md +17 -1
  5. {utterplan-0.6.8 → utterplan-0.6.9}/docs/cli.md +3 -1
  6. {utterplan-0.6.8 → utterplan-0.6.9}/docs/debugging.md +13 -0
  7. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_cli_bundle_render_book.py +51 -0
  8. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_cli_bundle_render_document.py +50 -0
  9. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_renderplan_export.py +36 -0
  10. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_renderplan_v7_contract.py +33 -0
  11. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/_book_service.py +7 -5
  12. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/_document_service.py +7 -5
  13. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/_version.py +3 -3
  14. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/bundle_replan.py +4 -2
  15. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/cli.py +13 -4
  16. utterplan-0.6.9/utterplan/renderprofile_io.py +59 -0
  17. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan.egg-info/PKG-INFO +11 -1
  18. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan.egg-info/SOURCES.txt +1 -0
  19. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan.egg-info/scm_file_list.json +14 -0
  20. utterplan-0.6.9/utterplan.egg-info/scm_version.json +8 -0
  21. utterplan-0.6.8/utterplan.egg-info/scm_version.json +0 -8
  22. {utterplan-0.6.8 → utterplan-0.6.9}/.codecrate.toml +0 -0
  23. {utterplan-0.6.8 → utterplan-0.6.9}/.github/workflows/codecov.yml +0 -0
  24. {utterplan-0.6.8 → utterplan-0.6.9}/.github/workflows/pre-commit.yml +0 -0
  25. {utterplan-0.6.8 → utterplan-0.6.9}/.github/workflows/python-publish.yml +0 -0
  26. {utterplan-0.6.8 → utterplan-0.6.9}/.github/workflows/tests.yml +0 -0
  27. {utterplan-0.6.8 → utterplan-0.6.9}/.gitignore +0 -0
  28. {utterplan-0.6.8 → utterplan-0.6.9}/.pre-commit-config.yaml +0 -0
  29. {utterplan-0.6.8 → utterplan-0.6.9}/AGENTS.md +0 -0
  30. {utterplan-0.6.8 → utterplan-0.6.9}/LICENSE +0 -0
  31. {utterplan-0.6.8 → utterplan-0.6.9}/MANIFEST.in +0 -0
  32. {utterplan-0.6.8 → utterplan-0.6.9}/NOTICE +0 -0
  33. {utterplan-0.6.8 → utterplan-0.6.9}/docs/Makefile +0 -0
  34. {utterplan-0.6.8 → utterplan-0.6.9}/docs/architecture.md +0 -0
  35. {utterplan-0.6.8 → utterplan-0.6.9}/docs/conf.py +0 -0
  36. {utterplan-0.6.8 → utterplan-0.6.9}/docs/consumer-guide.md +0 -0
  37. {utterplan-0.6.8 → utterplan-0.6.9}/docs/coordinate-spaces.md +0 -0
  38. {utterplan-0.6.8 → utterplan-0.6.9}/docs/format.md +0 -0
  39. {utterplan-0.6.8 → utterplan-0.6.9}/docs/getting-started.md +0 -0
  40. {utterplan-0.6.8 → utterplan-0.6.9}/docs/index.md +0 -0
  41. {utterplan-0.6.8 → utterplan-0.6.9}/docs/make.bat +0 -0
  42. {utterplan-0.6.8 → utterplan-0.6.9}/docs/phase-a-completion.md +0 -0
  43. {utterplan-0.6.8 → utterplan-0.6.9}/docs/pykokoro-integration.md +0 -0
  44. {utterplan-0.6.8 → utterplan-0.6.9}/docs/python-api.md +0 -0
  45. {utterplan-0.6.8 → utterplan-0.6.9}/docs/requirements.txt +0 -0
  46. {utterplan-0.6.8 → utterplan-0.6.9}/examples/basic.py +0 -0
  47. {utterplan-0.6.8 → utterplan-0.6.9}/examples/chapter.ssmd +0 -0
  48. {utterplan-0.6.8 → utterplan-0.6.9}/examples/compile_file.py +0 -0
  49. {utterplan-0.6.8 → utterplan-0.6.9}/examples/inspect_plan.py +0 -0
  50. {utterplan-0.6.8 → utterplan-0.6.9}/examples/multilingual.py +0 -0
  51. {utterplan-0.6.8 → utterplan-0.6.9}/examples/ssmd_example.py +0 -0
  52. {utterplan-0.6.8 → utterplan-0.6.9}/pyproject.toml +0 -0
  53. {utterplan-0.6.8 → utterplan-0.6.9}/setup.cfg +0 -0
  54. {utterplan-0.6.8 → utterplan-0.6.9}/spec/schemas/v1.schema.json +0 -0
  55. {utterplan-0.6.8 → utterplan-0.6.9}/spec/schemas/v2.schema.json +0 -0
  56. {utterplan-0.6.8 → utterplan-0.6.9}/spec/schemas/v3.schema.json +0 -0
  57. {utterplan-0.6.8 → utterplan-0.6.9}/spec/schemas/v4.schema.json +0 -0
  58. {utterplan-0.6.8 → utterplan-0.6.9}/spec/utterplan.schema.json +0 -0
  59. {utterplan-0.6.8 → utterplan-0.6.9}/tests/__init__.py +0 -0
  60. {utterplan-0.6.8 → utterplan-0.6.9}/tests/compiler_helpers.py +0 -0
  61. {utterplan-0.6.8 → utterplan-0.6.9}/tests/fixtures/canonical_consumer_contract.expected.toml +0 -0
  62. {utterplan-0.6.8 → utterplan-0.6.9}/tests/fixtures/canonical_consumer_contract.ssmd +0 -0
  63. {utterplan-0.6.8 → utterplan-0.6.9}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/document.ssmd.md +0 -0
  64. {utterplan-0.6.8 → utterplan-0.6.9}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/manifest.json +0 -0
  65. {utterplan-0.6.8 → utterplan-0.6.9}/tests/fixtures/ssmd_09_comprehensive.ssmd +0 -0
  66. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/basic_en.utterplan.toml +0 -0
  67. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/directives.utterplan.toml +0 -0
  68. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/markers.utterplan.toml +0 -0
  69. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/multilingual.utterplan.toml +0 -0
  70. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/parenthetical.utterplan.toml +0 -0
  71. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/spokenform_offsets.utterplan.toml +0 -0
  72. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/ssmd_09_comprehensive.utterplan.toml +0 -0
  73. {utterplan-0.6.8 → utterplan-0.6.9}/tests/golden/ssmd_breaks.utterplan.toml +0 -0
  74. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/__init__.py +0 -0
  75. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/cases.py +0 -0
  76. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/basic_en.utterplan.json +0 -0
  77. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/directives.utterplan.json +0 -0
  78. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/markers.utterplan.json +0 -0
  79. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/multilingual.utterplan.json +0 -0
  80. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/parenthetical.utterplan.json +0 -0
  81. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/spokenform_offsets.utterplan.json +0 -0
  82. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/ssmd_09_comprehensive.utterplan.json +0 -0
  83. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/fixtures/v4/ssmd_breaks.utterplan.json +0 -0
  84. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/goldens/multilingual_preparation.json +0 -0
  85. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/goldens/spokenform_numbers.json +0 -0
  86. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/goldens/ssmd_break.json +0 -0
  87. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/goldens/ssmd_language_detection.json +0 -0
  88. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/goldens/ssmd_marker.json +0 -0
  89. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/goldens/ssmd_pronunciation.json +0 -0
  90. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/test_legacy_json_import.py +0 -0
  91. {utterplan-0.6.8 → utterplan-0.6.9}/tests/migration/test_semantic_migration_regressions.py +0 -0
  92. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/README.md +0 -0
  93. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v1/basic.json +0 -0
  94. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v1/directives.json +0 -0
  95. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v1/markers.json +0 -0
  96. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v1/multilingual.json +0 -0
  97. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v1/parenthetical.json +0 -0
  98. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v1/spokenform_offsets.json +0 -0
  99. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v1/ssmd_breaks.json +0 -0
  100. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v2/directives.json +0 -0
  101. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v3/multilingual.json +0 -0
  102. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v3/ssmd_09_comprehensive.json +0 -0
  103. {utterplan-0.6.8 → utterplan-0.6.9}/tests/schema_history/v5/canonical_consumer_contract.toml +0 -0
  104. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_atomic_io.py +0 -0
  105. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_batch.py +0 -0
  106. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_boundaries.py +0 -0
  107. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_bundle_book_replan.py +0 -0
  108. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_bundle_book_services.py +0 -0
  109. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_bundle_document_compile.py +0 -0
  110. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_bundle_document_render.py +0 -0
  111. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_bundle_replan.py +0 -0
  112. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_bundle_role_inventory.py +0 -0
  113. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_bundle_services.py +0 -0
  114. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_cli.py +0 -0
  115. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_cli_batch.py +0 -0
  116. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_cli_bundle_compile_book.py +0 -0
  117. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_cli_bundle_document.py +0 -0
  118. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_cli_bundle_replan_book.py +0 -0
  119. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_compiler.py +0 -0
  120. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_consumer_contract.py +0 -0
  121. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_consumer_readiness.py +0 -0
  122. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_coordinates.py +0 -0
  123. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_dependency_architecture.py +0 -0
  124. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_directives.py +0 -0
  125. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_explain.py +0 -0
  126. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_fake_renderer.py +0 -0
  127. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_flow_compilation.py +0 -0
  128. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_format.py +0 -0
  129. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_golden.py +0 -0
  130. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_identity_units.py +0 -0
  131. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_import_boundaries.py +0 -0
  132. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_language.py +0 -0
  133. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_linguistics.py +0 -0
  134. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_migration_framework.py +0 -0
  135. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_migration_v3_to_v4.py +0 -0
  136. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_migration_v4_to_v5.py +0 -0
  137. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_pauses.py +0 -0
  138. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_planner.py +0 -0
  139. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_preparation.py +0 -0
  140. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_preview.py +0 -0
  141. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_progress.py +0 -0
  142. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_public_identity.py +0 -0
  143. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_renderability.py +0 -0
  144. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_renderability_repair.py +0 -0
  145. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_renderplan_batch.py +0 -0
  146. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_renderplan_dependency_contract.py +0 -0
  147. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_renderplan_replan.py +0 -0
  148. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_replacement_provenance.py +0 -0
  149. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_role_coverage.py +0 -0
  150. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_schema_history.py +0 -0
  151. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_schema_registry.py +0 -0
  152. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_schema_strict.py +0 -0
  153. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_schema_v6_source.py +0 -0
  154. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_schema_v7.py +0 -0
  155. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_segment_topology.py +0 -0
  156. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_segmentation_adapter.py +0 -0
  157. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_segmentation_recovery.py +0 -0
  158. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_semantic_boundaries.py +0 -0
  159. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_sentence_topology.py +0 -0
  160. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_source_input.py +0 -0
  161. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_ssmd_09_parser.py +0 -0
  162. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_ssmd_bundle.py +0 -0
  163. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_ssmd_bundle_book.py +0 -0
  164. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_ssmd_bundle_document.py +0 -0
  165. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_ssmd_substitution_preparation.py +0 -0
  166. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_token_topology.py +0 -0
  167. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_toml_format.py +0 -0
  168. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_ttsready_preparation_adapter.py +0 -0
  169. {utterplan-0.6.8 → utterplan-0.6.9}/tests/test_version.py +0 -0
  170. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/__init__.py +0 -0
  171. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/__main__.py +0 -0
  172. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/_book_replan.py +0 -0
  173. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/atomic_io.py +0 -0
  174. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/attempt_serialization.py +0 -0
  175. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/attempts.py +0 -0
  176. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/batch.py +0 -0
  177. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/boundaries.py +0 -0
  178. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/bundle.py +0 -0
  179. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/bundle_role_inventory.py +0 -0
  180. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/codecs/__init__.py +0 -0
  181. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/codecs/v4_toml.py +0 -0
  182. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/codecs/v5_toml.py +0 -0
  183. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/codecs/v6_toml.py +0 -0
  184. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/codecs/v7_toml.py +0 -0
  185. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/compiler.py +0 -0
  186. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/config.py +0 -0
  187. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/diagnostics.py +0 -0
  188. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/directives.py +0 -0
  189. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/exceptions.py +0 -0
  190. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/explain.py +0 -0
  191. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/flow_projection.py +0 -0
  192. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/format.py +0 -0
  193. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/hashing.py +0 -0
  194. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/language.py +0 -0
  195. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/linguistics.py +0 -0
  196. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migration.py +0 -0
  197. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/__init__.py +0 -0
  198. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/registry.py +0 -0
  199. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/v1_to_v2.py +0 -0
  200. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/v2_to_v3.py +0 -0
  201. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/v3_to_v4.py +0 -0
  202. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/v4_to_v5.py +0 -0
  203. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/v5_to_v6.py +0 -0
  204. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/migrations/v6_to_v7.py +0 -0
  205. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/model.py +0 -0
  206. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/parsers.py +0 -0
  207. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/pauses.py +0 -0
  208. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/planner.py +0 -0
  209. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/preparation.py +0 -0
  210. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/preview.py +0 -0
  211. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/progress.py +0 -0
  212. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/py.typed +0 -0
  213. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/renderability.py +0 -0
  214. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/renderplan_batch.py +0 -0
  215. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/renderplan_export.py +0 -0
  216. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/renderplan_grouping.py +0 -0
  217. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/renderplan_projection.py +0 -0
  218. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/renderplan_replan.py +0 -0
  219. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/renderplan_workspace.py +0 -0
  220. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/role_coverage.py +0 -0
  221. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/schema_registry.py +0 -0
  222. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/schemas/v1.schema.json +0 -0
  223. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/schemas/v2.schema.json +0 -0
  224. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/schemas/v3.schema.json +0 -0
  225. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/schemas/v4.schema.json +0 -0
  226. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/segmentation.py +0 -0
  227. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/serialization.py +0 -0
  228. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/source_input.py +0 -0
  229. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/spacy_models.py +0 -0
  230. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/spans.py +0 -0
  231. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/ssmd_bundle.py +0 -0
  232. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/__init__.py +0 -0
  233. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/document/__init__.py +0 -0
  234. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/document/plain.py +0 -0
  235. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/document/ssmd.py +0 -0
  236. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/preparation/__init__.py +0 -0
  237. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/preparation/identity.py +0 -0
  238. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/preparation/ssmd.py +0 -0
  239. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/preparation/ttsready.py +0 -0
  240. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/stages/protocols.py +0 -0
  241. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/token_topology.py +0 -0
  242. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/toml_codec.py +0 -0
  243. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/trace_codec.py +0 -0
  244. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/units.py +0 -0
  245. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/utterplan.schema.json +0 -0
  246. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/versioning.py +0 -0
  247. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan/workspace_paths.py +0 -0
  248. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan.egg-info/dependency_links.txt +0 -0
  249. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan.egg-info/entry_points.txt +0 -0
  250. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan.egg-info/requires.txt +0 -0
  251. {utterplan-0.6.8 → utterplan-0.6.9}/utterplan.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utterplan
3
- Version: 0.6.8
3
+ Version: 0.6.9
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
@@ -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
 
@@ -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,23 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ## [v0.6.8] - Unreleased
7
+ ## [v0.6.9] - 2026-10-27
8
+
9
+ ### Added
10
+
11
+ - Added shared RenderProfile loading with canonical path resolution, validation, and actionable stale-identity diagnostics
12
+
13
+ ### Changed
14
+
15
+ - Changed bundle rendering and replanning to use validated profile identities and keep --force limited to valid outputs
16
+
17
+ ### Fixed
18
+
19
+ - Fixed token projection to preserve optional morphology and report missing required fields with locators
20
+
21
+ <!-- generated by releaseledger -->
22
+
23
+ ## [v0.6.8] - 2026-10-23
8
24
 
9
25
  ### Added
10
26
 
@@ -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.
@@ -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(
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  from pathlib import Path
5
5
 
6
+ import pytest
6
7
  from renderplan import (
7
8
  PausePolicy,
8
9
  PlanningPolicy,
@@ -395,3 +396,38 @@ def test_inspect_roles_reports_source_inventory_without_profile(tmp_path: Path,
395
396
  assert "host 1 speech segments" in output
396
397
  assert "guest 1 speech segments" in output
397
398
  assert "required by 1 speech segments" in output
399
+
400
+
401
+ @pytest.mark.parametrize("force", (False, True))
402
+ def test_render_plan_stale_profile_is_rejected_before_output(
403
+ tmp_path: Path, capsys, force: bool
404
+ ) -> None:
405
+ source_path = tmp_path / "chapter.utterplan.toml"
406
+ profile_path = tmp_path / "stale.renderprofile.toml"
407
+ output_path = tmp_path / "chapter.renderplan.toml"
408
+ _source().save(source_path)
409
+ renderplan_codec.save_profile(_profile(), profile_path)
410
+ profile_path.write_text(
411
+ profile_path.read_text(encoding="utf-8").replace(
412
+ 'engine = "fixture"', 'engine = "changed"'
413
+ ),
414
+ encoding="utf-8",
415
+ )
416
+ args = [
417
+ "render-plan",
418
+ str(source_path),
419
+ "--profile",
420
+ str(profile_path),
421
+ "--output",
422
+ str(output_path),
423
+ ]
424
+ if force:
425
+ args.append("--force")
426
+
427
+ assert main(args) == 1
428
+ error = capsys.readouterr().err
429
+ assert f"invalid RenderProfile {profile_path.resolve()}" in error
430
+ assert "profile_id does not match profile content" in error
431
+ assert "Regenerate or re-save the RenderProfile with its producer" in error
432
+ assert "--force does not bypass profile identity validation" in error
433
+ assert not output_path.exists()
@@ -127,6 +127,39 @@ def test_exact_source_text_tokens_and_controls_rebase_without_trimming() -> None
127
127
  }
128
128
 
129
129
 
130
+ def test_sparse_optional_morphology_is_preserved_but_required_morph_fails() -> None:
131
+ plan = FlowPlan(
132
+ language="en-US",
133
+ flow=(
134
+ FlowUnit(
135
+ (
136
+ FlowSegment(
137
+ "read the book",
138
+ "en-US",
139
+ tokens=(
140
+ TokenView(0, 4, lemma="read", morph="Tense=Pres"),
141
+ TokenView(5, 8, lemma="the"),
142
+ TokenView(9, 13, lemma="book", morph="Number=Sing"),
143
+ ),
144
+ ),
145
+ )
146
+ ),
147
+ ),
148
+ )
149
+ index = build_source_index(plan)
150
+ source = (SourceSpan("f000000.s000", 0, 13),)
151
+
152
+ optional = index.tokens_for(source, required_fields=())
153
+ assert optional is not None
154
+ assert optional.morph == ((0, "Tense=Pres"), (2, "Number=Sing"))
155
+
156
+ with pytest.raises(RenderPlanProjectionError, match="required token field 'morph'") as error:
157
+ index.tokens_for(source, required_fields=("morph",))
158
+ assert error.value.code == "renderplan.token.required"
159
+ assert error.value.locator == "f000000.s000"
160
+ assert error.value.token_index == 1
161
+
162
+
130
163
  def test_partial_token_slice_and_missing_required_field_fail_with_locator() -> None:
131
164
  index = build_source_index(_projection_plan())
132
165
 
@@ -26,6 +26,7 @@ from .batch import BatchProgressCallback, CompileOutcome, CompileRequest, compil
26
26
  from .config import PlannerConfig
27
27
  from .model import FlowPlan
28
28
  from .renderplan_batch import RenderOutcome, RenderRequest, new_renderplan_set, render_to_files
29
+ from .renderprofile_io import RenderProfileLoadError, load_render_profile
29
30
  from .role_coverage import profile_role_coverage
30
31
  from .ssmd_bundle import (
31
32
  SsmdBundleError,
@@ -335,11 +336,12 @@ def render_book(
335
336
 
336
337
  profile_input = request.profile.expanduser()
337
338
  try:
338
- active_profile = renderplan_codec.load_profile(profile_input)
339
- profile_path = profile_input.resolve(strict=True)
340
- except (OSError, RuntimeError, ValueError, TypeError) as exc:
341
- raise BookServiceError(f"invalid RenderProfile {profile_input}: {exc}") from exc
342
- profile_id = active_profile.profile_id or active_profile.computed_id()
339
+ loaded_profile = load_render_profile(profile_input)
340
+ except RenderProfileLoadError as exc:
341
+ raise BookServiceError(str(exc)) from exc
342
+ active_profile = loaded_profile.profile
343
+ profile_path = loaded_profile.path
344
+ profile_id = loaded_profile.profile_id
343
345
  fingerprint = profile_id.removeprefix("sha256:")[:12]
344
346
  label = _render_variant_label(profile_path, request.variant)
345
347
  variant_name = f"{label}--{fingerprint}"
@@ -16,6 +16,7 @@ from .config import PlannerConfig
16
16
  from .model import FlowPlan
17
17
  from .renderplan_export import build_render_plan
18
18
  from .renderplan_workspace import profile_fingerprint, render_variant_label
19
+ from .renderprofile_io import load_render_profile
19
20
  from .role_coverage import assert_profile_role_coverage
20
21
  from .ssmd_bundle import SsmdBundleError, SsmdBundleIndex, bundle_item_path, load_ssmd_bundle
21
22
  from .workspace_paths import renderplan_output_name, utterplan_output_name
@@ -249,10 +250,11 @@ def _render_document_service(
249
250
  )
250
251
  source_plan = FlowPlan.load(source_plan_path)
251
252
 
252
- profile_path = Path(profile).expanduser().resolve(strict=True)
253
- active_profile = renderplan_codec.load_profile(profile_path)
253
+ loaded_profile = load_render_profile(profile)
254
+ profile_path = loaded_profile.path
255
+ active_profile = loaded_profile.profile
254
256
  coverage = assert_profile_role_coverage(source_plan, active_profile)
255
- profile_id = active_profile.profile_id or active_profile.computed_id()
257
+ profile_id = loaded_profile.profile_id
256
258
  variant_name = (
257
259
  f"{render_variant_label(profile_path, variant)}--{profile_fingerprint(profile_id)}"
258
260
  )
@@ -326,8 +328,8 @@ def _render_document_service(
326
328
  raise _DocumentBundleServiceError(
327
329
  f"render profile snapshot must not be a symlink: {snapshot_path}"
328
330
  )
329
- existing_profile = renderplan_codec.load_profile(snapshot_path)
330
- existing_id = existing_profile.profile_id or existing_profile.computed_id()
331
+ existing_profile = load_render_profile(snapshot_path)
332
+ existing_id = existing_profile.profile_id
331
333
  if existing_id != profile_id:
332
334
  raise _DocumentBundleServiceError(
333
335
  "existing variant snapshot has a different profile identity"
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.6.8'
22
- __version_tuple__ = version_tuple = (0, 6, 8)
21
+ __version__ = version = '0.6.9'
22
+ __version_tuple__ = version_tuple = (0, 6, 9)
23
23
 
24
- __commit_id__ = commit_id = 'g167978804'
24
+ __commit_id__ = commit_id = 'g898a83db9'
@@ -23,6 +23,7 @@ from .atomic_io import atomic_write_text
23
23
  from .model import FlowPlan
24
24
  from .renderplan_export import build_render_plan
25
25
  from .renderplan_workspace import profile_fingerprint
26
+ from .renderprofile_io import load_render_profile
26
27
  from .ssmd_bundle import (
27
28
  BundleItem,
28
29
  BundleKind,
@@ -298,8 +299,9 @@ def _prepare_document(
298
299
  stage="load",
299
300
  )
300
301
  try:
301
- profile = renderplan_codec.load_profile(profile_path)
302
- profile_id = profile.profile_id or profile.computed_id()
302
+ loaded_profile = load_render_profile(profile_path)
303
+ profile = loaded_profile.profile
304
+ profile_id = loaded_profile.profile_id
303
305
  active = renderplan_codec.load_plan(active_path)
304
306
  active.validate_against_profile(profile)
305
307
  except (OSError, ValueError, TypeError) as exc:
@@ -45,6 +45,7 @@ from .preview import PreviewChunk, preview_plan
45
45
  from .renderability import format_renderability_error
46
46
  from .renderplan_batch import RenderOutcome
47
47
  from .renderplan_export import build_render_plan, format_render_plan_explanation
48
+ from .renderprofile_io import load_render_profile
48
49
  from .role_coverage import logical_role_inventory, profile_role_coverage
49
50
  from .source_input import _format_for_path as _format_source_path
50
51
  from .source_input import load_source_path
@@ -217,7 +218,13 @@ def build_parser() -> argparse.ArgumentParser:
217
218
  bundle_render.add_argument("--utterplan-dir", type=Path)
218
219
  bundle_render.add_argument("--output-root", type=Path)
219
220
  bundle_render.add_argument("--report", type=Path)
220
- bundle_render.add_argument("--force", action="store_true")
221
+ bundle_render.add_argument(
222
+ "--force",
223
+ action="store_true",
224
+ help=(
225
+ "replace valid existing RenderPlan outputs; does not bypass RenderProfile validation"
226
+ ),
227
+ )
221
228
  bundle_render.add_argument("--fail-fast", action="store_true")
222
229
 
223
230
  bundle_replan = bundle_commands.add_parser("replan", help="refine active bundle RenderPlans")
@@ -317,7 +324,9 @@ def build_parser() -> argparse.ArgumentParser:
317
324
  "-o", "--output", type=Path, required=True, help="write the RenderPlan TOML"
318
325
  )
319
326
  render_plan_parser.add_argument(
320
- "--force", action="store_true", help="replace an existing output file"
327
+ "--force",
328
+ action="store_true",
329
+ help="replace valid existing output; does not bypass profile validation",
321
330
  )
322
331
  render_plan_parser.add_argument(
323
332
  "--explain", action="store_true", help="print in-memory planning decisions"
@@ -899,7 +908,7 @@ def _replan_explanation(outcome: ReplanOutcome, profile_path: Path) -> str:
899
908
  ):
900
909
  return ""
901
910
  source_plan = FlowPlan.load(outcome.utterplan)
902
- profile = renderplan_codec.load_profile(profile_path)
911
+ profile = load_render_profile(profile_path).profile
903
912
  previous = renderplan_codec.load_plan(outcome.previous_renderplan)
904
913
  preflight = renderplan_codec.load_preflight(outcome.preflight)
905
914
  plan = build_render_plan(
@@ -1121,7 +1130,7 @@ def _render_plan_command(args: argparse.Namespace) -> int:
1121
1130
  raise _CliUsageError(f"output exists: {args.output}; use --force to replace it")
1122
1131
 
1123
1132
  source = _load_plan_file(args.input)
1124
- profile = renderplan_codec.load_profile(args.profile)
1133
+ profile = load_render_profile(args.profile).profile
1125
1134
  coverage = profile_role_coverage(source, profile)
1126
1135
  previous = renderplan_codec.load_plan(args.previous) if args.previous is not None else None
1127
1136
  preflight = (
@@ -0,0 +1,59 @@
1
+ """Shared loading and diagnostics for engine-backed RenderProfiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from renderplan import RenderProfile
9
+ from renderplan import codec as renderplan_codec
10
+
11
+
12
+ class RenderProfileLoadError(ValueError):
13
+ """A RenderProfile could not be resolved or failed RenderPlan validation."""
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class LoadedRenderProfile:
18
+ """A validated profile, its canonical path, and its effective identity."""
19
+
20
+ path: Path
21
+ profile: RenderProfile
22
+ profile_id: str
23
+
24
+
25
+ def render_profile_error_message(path: Path, exc: Exception) -> str:
26
+ """Add a stable UtterPlan boundary and remediation to profile-load errors."""
27
+ detail = str(exc)
28
+ message = f"invalid RenderProfile {path}: {detail}"
29
+ if detail == "profile_id does not match profile content":
30
+ return (
31
+ f"{message}.\n"
32
+ "The stored profile_id is stale for the current profile semantics. "
33
+ "Regenerate or re-save the RenderProfile with its producer and validate it "
34
+ "before rendering. --force does not bypass profile identity validation."
35
+ )
36
+ return message
37
+
38
+
39
+ def load_render_profile(path: str | Path) -> LoadedRenderProfile:
40
+ """Resolve and validate a profile using RenderPlan's canonical contract.
41
+
42
+ This function deliberately does not repair or recompute a persisted identity.
43
+ """
44
+ requested = Path(path).expanduser()
45
+ try:
46
+ resolved_path = requested.resolve(strict=True)
47
+ profile = renderplan_codec.load_profile(resolved_path)
48
+ except (OSError, RuntimeError, ValueError, TypeError) as exc:
49
+ try:
50
+ display_path = requested.resolve(strict=False)
51
+ except (OSError, RuntimeError):
52
+ display_path = requested.absolute()
53
+ raise RenderProfileLoadError(render_profile_error_message(display_path, exc)) from exc
54
+
55
+ return LoadedRenderProfile(
56
+ path=resolved_path,
57
+ profile=profile,
58
+ profile_id=profile.profile_id or profile.computed_id(),
59
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utterplan
3
- Version: 0.6.8
3
+ Version: 0.6.9
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
@@ -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
 
@@ -196,6 +196,7 @@ utterplan/renderplan_grouping.py
196
196
  utterplan/renderplan_projection.py
197
197
  utterplan/renderplan_replan.py
198
198
  utterplan/renderplan_workspace.py
199
+ utterplan/renderprofile_io.py
199
200
  utterplan/role_coverage.py
200
201
  utterplan/schema_registry.py
201
202
  utterplan/segmentation.py
@@ -167,6 +167,11 @@
167
167
  ".ledger/releaseledger/data/ledgers/main/releases/v0.6.7/release.md",
168
168
  ".ledger/releaseledger/data/ledgers/main/releases/v0.6.8/entries/entry-0001.md",
169
169
  ".ledger/releaseledger/data/ledgers/main/releases/v0.6.8/release.md",
170
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.6.9/audit/commit-audit.yaml",
171
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.6.9/entries/entry-0001.md",
172
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.6.9/entries/entry-0002.md",
173
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.6.9/entries/entry-0003.md",
174
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.6.9/release.md",
170
175
  ".ledger/releaseledger/work-v061-audit-decisions.yaml",
171
176
  ".ledger/releaseledger/work-v061-entries.yaml",
172
177
  ".ledger/releaseledger/work/0.1.2/audit-decisions.yaml",
@@ -298,6 +303,14 @@
298
303
  ".ledger/releaseledger/work/v0.6.6/evidence/5fac993.patch",
299
304
  ".ledger/releaseledger/work/v0.6.6/evidence/manifest.json",
300
305
  ".ledger/releaseledger/work/v0.6.6/range.json",
306
+ ".ledger/releaseledger/work/v0.6.9/audit-decisions.yaml",
307
+ ".ledger/releaseledger/work/v0.6.9/audit.yaml",
308
+ ".ledger/releaseledger/work/v0.6.9/entries.yaml",
309
+ ".ledger/releaseledger/work/v0.6.9/evidence/3b80a28.patch",
310
+ ".ledger/releaseledger/work/v0.6.9/evidence/740d814.patch",
311
+ ".ledger/releaseledger/work/v0.6.9/evidence/75db97f.patch",
312
+ ".ledger/releaseledger/work/v0.6.9/evidence/manifest.json",
313
+ ".ledger/releaseledger/work/v0.6.9/range.json",
301
314
  ".ledger/releaseledger/write.lock",
302
315
  ".ledger/taskledger/.ledger-project.toml",
303
316
  ".ledger/taskledger/config.toml",
@@ -505,6 +518,7 @@
505
518
  "utterplan/renderplan_projection.py",
506
519
  "utterplan/renderplan_replan.py",
507
520
  "utterplan/renderplan_workspace.py",
521
+ "utterplan/renderprofile_io.py",
508
522
  "utterplan/role_coverage.py",
509
523
  "utterplan/schema_registry.py",
510
524
  "utterplan/schemas/v1.schema.json",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.6.9",
3
+ "distance": 0,
4
+ "node": "g898a83db9a23fc2818d1ee63320013033e8df18c",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-27"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.6.8",
3
- "distance": 0,
4
- "node": "g167978804dbbfe99b051c9628c20f109f1d6e552",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-23"
8
- }
File without changes