utterplan 0.6.7__tar.gz → 0.6.8__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 (250) hide show
  1. {utterplan-0.6.7 → utterplan-0.6.8}/PKG-INFO +1 -1
  2. {utterplan-0.6.7 → utterplan-0.6.8}/docs/bundles.md +28 -0
  3. {utterplan-0.6.7 → utterplan-0.6.8}/docs/changelog.md +8 -0
  4. {utterplan-0.6.7 → utterplan-0.6.8}/docs/python-api.md +35 -0
  5. utterplan-0.6.8/tests/test_bundle_role_inventory.py +380 -0
  6. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_bundle_services.py +65 -0
  7. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_cli_bundle_compile_book.py +2 -1
  8. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_cli_bundle_document.py +9 -0
  9. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/__init__.py +16 -0
  10. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/_version.py +3 -3
  11. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/bundle.py +80 -1
  12. utterplan-0.6.8/utterplan/bundle_role_inventory.py +498 -0
  13. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/cli.py +12 -0
  14. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan.egg-info/PKG-INFO +1 -1
  15. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan.egg-info/SOURCES.txt +2 -0
  16. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan.egg-info/scm_file_list.json +4 -0
  17. utterplan-0.6.8/utterplan.egg-info/scm_version.json +8 -0
  18. utterplan-0.6.7/utterplan.egg-info/scm_version.json +0 -8
  19. {utterplan-0.6.7 → utterplan-0.6.8}/.codecrate.toml +0 -0
  20. {utterplan-0.6.7 → utterplan-0.6.8}/.github/workflows/codecov.yml +0 -0
  21. {utterplan-0.6.7 → utterplan-0.6.8}/.github/workflows/pre-commit.yml +0 -0
  22. {utterplan-0.6.7 → utterplan-0.6.8}/.github/workflows/python-publish.yml +0 -0
  23. {utterplan-0.6.7 → utterplan-0.6.8}/.github/workflows/tests.yml +0 -0
  24. {utterplan-0.6.7 → utterplan-0.6.8}/.gitignore +0 -0
  25. {utterplan-0.6.7 → utterplan-0.6.8}/.pre-commit-config.yaml +0 -0
  26. {utterplan-0.6.7 → utterplan-0.6.8}/AGENTS.md +0 -0
  27. {utterplan-0.6.7 → utterplan-0.6.8}/LICENSE +0 -0
  28. {utterplan-0.6.7 → utterplan-0.6.8}/MANIFEST.in +0 -0
  29. {utterplan-0.6.7 → utterplan-0.6.8}/NOTICE +0 -0
  30. {utterplan-0.6.7 → utterplan-0.6.8}/README.md +0 -0
  31. {utterplan-0.6.7 → utterplan-0.6.8}/docs/Makefile +0 -0
  32. {utterplan-0.6.7 → utterplan-0.6.8}/docs/architecture.md +0 -0
  33. {utterplan-0.6.7 → utterplan-0.6.8}/docs/cli.md +0 -0
  34. {utterplan-0.6.7 → utterplan-0.6.8}/docs/conf.py +0 -0
  35. {utterplan-0.6.7 → utterplan-0.6.8}/docs/consumer-guide.md +0 -0
  36. {utterplan-0.6.7 → utterplan-0.6.8}/docs/coordinate-spaces.md +0 -0
  37. {utterplan-0.6.7 → utterplan-0.6.8}/docs/debugging.md +0 -0
  38. {utterplan-0.6.7 → utterplan-0.6.8}/docs/format.md +0 -0
  39. {utterplan-0.6.7 → utterplan-0.6.8}/docs/getting-started.md +0 -0
  40. {utterplan-0.6.7 → utterplan-0.6.8}/docs/index.md +0 -0
  41. {utterplan-0.6.7 → utterplan-0.6.8}/docs/make.bat +0 -0
  42. {utterplan-0.6.7 → utterplan-0.6.8}/docs/phase-a-completion.md +0 -0
  43. {utterplan-0.6.7 → utterplan-0.6.8}/docs/pykokoro-integration.md +0 -0
  44. {utterplan-0.6.7 → utterplan-0.6.8}/docs/requirements.txt +0 -0
  45. {utterplan-0.6.7 → utterplan-0.6.8}/examples/basic.py +0 -0
  46. {utterplan-0.6.7 → utterplan-0.6.8}/examples/chapter.ssmd +0 -0
  47. {utterplan-0.6.7 → utterplan-0.6.8}/examples/compile_file.py +0 -0
  48. {utterplan-0.6.7 → utterplan-0.6.8}/examples/inspect_plan.py +0 -0
  49. {utterplan-0.6.7 → utterplan-0.6.8}/examples/multilingual.py +0 -0
  50. {utterplan-0.6.7 → utterplan-0.6.8}/examples/ssmd_example.py +0 -0
  51. {utterplan-0.6.7 → utterplan-0.6.8}/pyproject.toml +0 -0
  52. {utterplan-0.6.7 → utterplan-0.6.8}/setup.cfg +0 -0
  53. {utterplan-0.6.7 → utterplan-0.6.8}/spec/schemas/v1.schema.json +0 -0
  54. {utterplan-0.6.7 → utterplan-0.6.8}/spec/schemas/v2.schema.json +0 -0
  55. {utterplan-0.6.7 → utterplan-0.6.8}/spec/schemas/v3.schema.json +0 -0
  56. {utterplan-0.6.7 → utterplan-0.6.8}/spec/schemas/v4.schema.json +0 -0
  57. {utterplan-0.6.7 → utterplan-0.6.8}/spec/utterplan.schema.json +0 -0
  58. {utterplan-0.6.7 → utterplan-0.6.8}/tests/__init__.py +0 -0
  59. {utterplan-0.6.7 → utterplan-0.6.8}/tests/compiler_helpers.py +0 -0
  60. {utterplan-0.6.7 → utterplan-0.6.8}/tests/fixtures/canonical_consumer_contract.expected.toml +0 -0
  61. {utterplan-0.6.7 → utterplan-0.6.8}/tests/fixtures/canonical_consumer_contract.ssmd +0 -0
  62. {utterplan-0.6.7 → utterplan-0.6.8}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/document.ssmd.md +0 -0
  63. {utterplan-0.6.7 → utterplan-0.6.8}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/manifest.json +0 -0
  64. {utterplan-0.6.7 → utterplan-0.6.8}/tests/fixtures/ssmd_09_comprehensive.ssmd +0 -0
  65. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/basic_en.utterplan.toml +0 -0
  66. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/directives.utterplan.toml +0 -0
  67. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/markers.utterplan.toml +0 -0
  68. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/multilingual.utterplan.toml +0 -0
  69. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/parenthetical.utterplan.toml +0 -0
  70. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/spokenform_offsets.utterplan.toml +0 -0
  71. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/ssmd_09_comprehensive.utterplan.toml +0 -0
  72. {utterplan-0.6.7 → utterplan-0.6.8}/tests/golden/ssmd_breaks.utterplan.toml +0 -0
  73. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/__init__.py +0 -0
  74. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/cases.py +0 -0
  75. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/basic_en.utterplan.json +0 -0
  76. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/directives.utterplan.json +0 -0
  77. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/markers.utterplan.json +0 -0
  78. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/multilingual.utterplan.json +0 -0
  79. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/parenthetical.utterplan.json +0 -0
  80. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/spokenform_offsets.utterplan.json +0 -0
  81. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/ssmd_09_comprehensive.utterplan.json +0 -0
  82. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/fixtures/v4/ssmd_breaks.utterplan.json +0 -0
  83. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/goldens/multilingual_preparation.json +0 -0
  84. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/goldens/spokenform_numbers.json +0 -0
  85. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/goldens/ssmd_break.json +0 -0
  86. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/goldens/ssmd_language_detection.json +0 -0
  87. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/goldens/ssmd_marker.json +0 -0
  88. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/goldens/ssmd_pronunciation.json +0 -0
  89. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/test_legacy_json_import.py +0 -0
  90. {utterplan-0.6.7 → utterplan-0.6.8}/tests/migration/test_semantic_migration_regressions.py +0 -0
  91. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/README.md +0 -0
  92. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v1/basic.json +0 -0
  93. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v1/directives.json +0 -0
  94. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v1/markers.json +0 -0
  95. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v1/multilingual.json +0 -0
  96. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v1/parenthetical.json +0 -0
  97. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v1/spokenform_offsets.json +0 -0
  98. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v1/ssmd_breaks.json +0 -0
  99. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v2/directives.json +0 -0
  100. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v3/multilingual.json +0 -0
  101. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v3/ssmd_09_comprehensive.json +0 -0
  102. {utterplan-0.6.7 → utterplan-0.6.8}/tests/schema_history/v5/canonical_consumer_contract.toml +0 -0
  103. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_atomic_io.py +0 -0
  104. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_batch.py +0 -0
  105. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_boundaries.py +0 -0
  106. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_bundle_book_replan.py +0 -0
  107. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_bundle_book_services.py +0 -0
  108. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_bundle_document_compile.py +0 -0
  109. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_bundle_document_render.py +0 -0
  110. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_bundle_replan.py +0 -0
  111. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_cli.py +0 -0
  112. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_cli_batch.py +0 -0
  113. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_cli_bundle_render_book.py +0 -0
  114. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_cli_bundle_render_document.py +0 -0
  115. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_cli_bundle_replan_book.py +0 -0
  116. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_compiler.py +0 -0
  117. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_consumer_contract.py +0 -0
  118. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_consumer_readiness.py +0 -0
  119. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_coordinates.py +0 -0
  120. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_dependency_architecture.py +0 -0
  121. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_directives.py +0 -0
  122. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_explain.py +0 -0
  123. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_fake_renderer.py +0 -0
  124. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_flow_compilation.py +0 -0
  125. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_format.py +0 -0
  126. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_golden.py +0 -0
  127. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_identity_units.py +0 -0
  128. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_import_boundaries.py +0 -0
  129. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_language.py +0 -0
  130. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_linguistics.py +0 -0
  131. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_migration_framework.py +0 -0
  132. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_migration_v3_to_v4.py +0 -0
  133. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_migration_v4_to_v5.py +0 -0
  134. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_pauses.py +0 -0
  135. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_planner.py +0 -0
  136. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_preparation.py +0 -0
  137. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_preview.py +0 -0
  138. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_progress.py +0 -0
  139. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_public_identity.py +0 -0
  140. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_renderability.py +0 -0
  141. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_renderability_repair.py +0 -0
  142. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_renderplan_batch.py +0 -0
  143. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_renderplan_dependency_contract.py +0 -0
  144. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_renderplan_export.py +0 -0
  145. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_renderplan_replan.py +0 -0
  146. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_renderplan_v7_contract.py +0 -0
  147. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_replacement_provenance.py +0 -0
  148. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_role_coverage.py +0 -0
  149. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_schema_history.py +0 -0
  150. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_schema_registry.py +0 -0
  151. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_schema_strict.py +0 -0
  152. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_schema_v6_source.py +0 -0
  153. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_schema_v7.py +0 -0
  154. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_segment_topology.py +0 -0
  155. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_segmentation_adapter.py +0 -0
  156. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_segmentation_recovery.py +0 -0
  157. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_semantic_boundaries.py +0 -0
  158. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_sentence_topology.py +0 -0
  159. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_source_input.py +0 -0
  160. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_ssmd_09_parser.py +0 -0
  161. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_ssmd_bundle.py +0 -0
  162. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_ssmd_bundle_book.py +0 -0
  163. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_ssmd_bundle_document.py +0 -0
  164. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_ssmd_substitution_preparation.py +0 -0
  165. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_token_topology.py +0 -0
  166. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_toml_format.py +0 -0
  167. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_ttsready_preparation_adapter.py +0 -0
  168. {utterplan-0.6.7 → utterplan-0.6.8}/tests/test_version.py +0 -0
  169. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/__main__.py +0 -0
  170. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/_book_replan.py +0 -0
  171. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/_book_service.py +0 -0
  172. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/_document_service.py +0 -0
  173. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/atomic_io.py +0 -0
  174. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/attempt_serialization.py +0 -0
  175. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/attempts.py +0 -0
  176. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/batch.py +0 -0
  177. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/boundaries.py +0 -0
  178. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/bundle_replan.py +0 -0
  179. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/codecs/__init__.py +0 -0
  180. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/codecs/v4_toml.py +0 -0
  181. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/codecs/v5_toml.py +0 -0
  182. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/codecs/v6_toml.py +0 -0
  183. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/codecs/v7_toml.py +0 -0
  184. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/compiler.py +0 -0
  185. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/config.py +0 -0
  186. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/diagnostics.py +0 -0
  187. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/directives.py +0 -0
  188. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/exceptions.py +0 -0
  189. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/explain.py +0 -0
  190. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/flow_projection.py +0 -0
  191. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/format.py +0 -0
  192. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/hashing.py +0 -0
  193. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/language.py +0 -0
  194. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/linguistics.py +0 -0
  195. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migration.py +0 -0
  196. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/__init__.py +0 -0
  197. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/registry.py +0 -0
  198. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/v1_to_v2.py +0 -0
  199. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/v2_to_v3.py +0 -0
  200. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/v3_to_v4.py +0 -0
  201. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/v4_to_v5.py +0 -0
  202. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/v5_to_v6.py +0 -0
  203. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/migrations/v6_to_v7.py +0 -0
  204. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/model.py +0 -0
  205. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/parsers.py +0 -0
  206. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/pauses.py +0 -0
  207. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/planner.py +0 -0
  208. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/preparation.py +0 -0
  209. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/preview.py +0 -0
  210. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/progress.py +0 -0
  211. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/py.typed +0 -0
  212. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/renderability.py +0 -0
  213. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/renderplan_batch.py +0 -0
  214. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/renderplan_export.py +0 -0
  215. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/renderplan_grouping.py +0 -0
  216. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/renderplan_projection.py +0 -0
  217. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/renderplan_replan.py +0 -0
  218. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/renderplan_workspace.py +0 -0
  219. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/role_coverage.py +0 -0
  220. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/schema_registry.py +0 -0
  221. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/schemas/v1.schema.json +0 -0
  222. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/schemas/v2.schema.json +0 -0
  223. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/schemas/v3.schema.json +0 -0
  224. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/schemas/v4.schema.json +0 -0
  225. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/segmentation.py +0 -0
  226. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/serialization.py +0 -0
  227. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/source_input.py +0 -0
  228. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/spacy_models.py +0 -0
  229. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/spans.py +0 -0
  230. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/ssmd_bundle.py +0 -0
  231. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/__init__.py +0 -0
  232. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/document/__init__.py +0 -0
  233. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/document/plain.py +0 -0
  234. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/document/ssmd.py +0 -0
  235. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/preparation/__init__.py +0 -0
  236. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/preparation/identity.py +0 -0
  237. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/preparation/ssmd.py +0 -0
  238. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/preparation/ttsready.py +0 -0
  239. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/stages/protocols.py +0 -0
  240. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/token_topology.py +0 -0
  241. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/toml_codec.py +0 -0
  242. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/trace_codec.py +0 -0
  243. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/units.py +0 -0
  244. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/utterplan.schema.json +0 -0
  245. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/versioning.py +0 -0
  246. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan/workspace_paths.py +0 -0
  247. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan.egg-info/dependency_links.txt +0 -0
  248. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan.egg-info/entry_points.txt +0 -0
  249. {utterplan-0.6.7 → utterplan-0.6.8}/utterplan.egg-info/requires.txt +0 -0
  250. {utterplan-0.6.7 → utterplan-0.6.8}/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.7
3
+ Version: 0.6.8
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
@@ -37,6 +37,32 @@ Compilation defaults to all items. For book bundles, `--chapters` selects manife
37
37
 
38
38
  By default, one schema-v7 `.utterplan.toml` plan per selected item and `compile-report.toml` are written under `<bundle>/utterplan/`. `--output-dir` and `--report` override those destinations. Existing plans are protected unless `--force` is supplied; `--fail-fast` records later selected items as skipped after a failure. `--language` is a fallback for SSMD language metadata. Bundle sources are SSMD 0.9, so the command does not accept an input-format override. The report is operational metadata and does not affect plan identity.
39
39
 
40
+ ### Bundle-level logical role inventory
41
+
42
+ A successful complete bundle compile writes `utterplan/role-inventory.toml` under the selected UtterPlan output directory (by default, `<bundle>/utterplan/`). For example:
43
+
44
+ ```bash
45
+ ssmdconvert bundle convert story.ssmd.md
46
+ utterplan bundle compile story.ssmd-bundle
47
+ ```
48
+
49
+ The bundle contains the source and compiled document plan alongside the derived inventory:
50
+
51
+ ```text
52
+ story.ssmd-bundle/
53
+ ├── document.ssmd.md
54
+ ├── manifest.json
55
+ └── utterplan/
56
+ ├── document.utterplan.toml
57
+ └── role-inventory.toml
58
+ ```
59
+
60
+ The TOML contract uses `format = "utterplan.bundle-role-inventory"` and `version = 1`. It records `bundle_kind`, a SHA-256 of the exact manifest bytes, aggregate `[[roles]]` with speech-segment occurrence counts, bundle-order `[[items]]`, and each item's source and plan paths, actual source-byte SHA-256, canonical `plan_id`, explicit role order/counts, and implicit/default-role count. Paths are bundle-relative POSIX paths, not absolute machine paths. Inventory bytes are deterministic and the file is replaced atomically.
61
+
62
+ Roles and counts derive from compiled FlowPlan speech segments through UtterPlan's logical role semantics, not by scanning SSMD markup or inspecting RenderPlans. Aggregate role order is first semantic occurrence across items in manifest order; item role order is first occurrence within that plan. Empty and audio-only segments do not count. Speech without an explicit role sets `uses_default_role = true` and increments `implicit_occurrences`; it is not assigned an invented role name. A profile-aware consumer resolves the concrete default role later.
63
+
64
+ Freshness is content-based: `manifest_sha256` hashes the current `manifest.json` bytes, each `source_sha256` hashes its current source bytes, and each item stores the compiled plan's `plan_id`. Modification times are not used. A subset book compile never replaces the canonical whole-bundle inventory and returns no inventory path. A failed full compile also leaves any previous inventory untouched. The successful full-compile summary reports explicit roles, whether a default role is required, and the inventory path; partial compiles do not claim a whole-bundle inventory was produced.
65
+
40
66
  ## Render
41
67
 
42
68
  Compilation and rendering are separate operations. Rendering reads saved plans and requires an explicit RenderProfile:
@@ -87,3 +113,5 @@ print(projected.variant)
87
113
  ```
88
114
 
89
115
  `compile_bundle()`, `render_bundle()`, and `replan_bundle()` accept paths and keyword options. Their result records expose the resolved bundle, kind, selection, per-item outcomes, artifact paths, reports, and exit status. Optional progress and start callbacks receive typed records; services do not print. Invalid or unsafe requests raise `BundleServiceError` or `BundleReplanError`, while item-level failures are returned in result outcomes.
116
+
117
+ For a successful full bundle compile, `BundleCompileResult.role_inventory_path` is the canonical `role-inventory.toml` path. It is `None` for partial selections and any compile with a failed item; in those cases the prior whole-bundle inventory, if present, remains untouched. The artifact is additive and does not alter plan identity or bundle rendering.
@@ -4,6 +4,14 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.6.8] - Unreleased
8
+
9
+ ### Added
10
+
11
+ - Added renderer-neutral role inventories to full bundle compilation
12
+
13
+ <!-- generated by releaseledger -->
14
+
7
15
  ## [v0.6.7] - 2026-10-22
8
16
 
9
17
  ### Fixed
@@ -236,6 +236,41 @@ The compile and render services accept paths and keyword options. `chapters` def
236
236
 
237
237
  `BundleCompileResult`, `BundleRenderResult`, and `BundleReplanResult` expose the resolved bundle, manifest-declared kind, selected item IDs, per-item outcomes, generated paths, reports, and exit status. Their start records provide resolved configuration to optional callbacks. Invalid or unsafe shared requests raise `BundleServiceError` or `BundleReplanError`; item-level failures are reported in result outcomes. The package also exports manifest loader, selector, item-path, result, start, and callback types for integrations that need to inspect dispatch results.
238
238
 
239
+ ## Bundle role inventory
240
+
241
+ `BundleRole`, `BundleRoleInventoryItem`, and `BundleRoleInventory` are immutable public models for the version-1 `utterplan.bundle-role-inventory` TOML interoperability contract. `build_bundle_role_inventory()` consumes compiled item records in manifest order; each record provides `id`, `source_path`, and `plan_path`, with optional `source_relative_path` and `plan_relative_path` values. It loads the saved FlowPlan, reuses `logical_role_inventory()`, and hashes the exact manifest and source file bytes.
242
+
243
+ ```python
244
+ from pathlib import Path
245
+
246
+ from utterplan import (
247
+ build_bundle_role_inventory,
248
+ load_bundle_role_inventory,
249
+ save_bundle_role_inventory,
250
+ )
251
+
252
+ bundle = Path("story.ssmd-bundle")
253
+ inventory = build_bundle_role_inventory(
254
+ bundle_root=bundle,
255
+ bundle_kind="document",
256
+ manifest_path=bundle / "manifest.json",
257
+ compiled_items=[
258
+ {
259
+ "id": "document-0001",
260
+ "source_path": bundle / "document.ssmd.md",
261
+ "source_relative_path": "document.ssmd.md",
262
+ "plan_path": bundle / "utterplan/document.utterplan.toml",
263
+ "plan_relative_path": "utterplan/document.utterplan.toml",
264
+ }
265
+ ],
266
+ )
267
+ save_bundle_role_inventory(inventory, bundle / "utterplan/role-inventory.toml")
268
+ restored = load_bundle_role_inventory(bundle / "utterplan/role-inventory.toml")
269
+ assert restored == inventory
270
+ ```
271
+
272
+ The loader rejects unsupported versions, malformed hashes/paths, duplicate roles or item IDs, invalid counts, inconsistent explicit-role maps, and aggregates that do not match their item records. Saving is deterministic and atomic. Persisted `uses_default_role` and `implicit_occurrences` describe unresolved profile-default speech; no concrete role is inferred. Item source and plan references are bundle-relative POSIX paths, and their hashes/`plan_id` let consumers detect stale metadata without modification-time checks. The bundle compiler emits this artifact only for successful complete selections; partial or failed compiles do not replace it. Downstream tools may read this serialized contract without importing UtterPlan or parsing SSMD.
273
+
239
274
  ## Planner progress callbacks
240
275
 
241
276
  `UtterancePlanner.plan`, `UtterancePlanner.compile`, and `compile_document` accept an optional keyword-only `on_progress` callback. It receives typed `PlannerProgressEvent` objects synchronously in the planner thread. Progress is operational only: it is not added to `PlannerConfig` or the plan, and enabling a callback does not change plan identity or serialization.
@@ -0,0 +1,380 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from types import SimpleNamespace
5
+
6
+ import pytest
7
+
8
+ import utterplan
9
+ from utterplan import (
10
+ AudioDirective,
11
+ FlowPlan,
12
+ FlowSegment,
13
+ FlowUnit,
14
+ SegmentDirectives,
15
+ VoiceDirective,
16
+ )
17
+ from utterplan.bundle_role_inventory import (
18
+ BundleRole,
19
+ BundleRoleInventory,
20
+ BundleRoleInventoryError,
21
+ BundleRoleInventoryItem,
22
+ build_bundle_role_inventory,
23
+ load_bundle_role_inventory,
24
+ save_bundle_role_inventory,
25
+ sha256_file,
26
+ )
27
+
28
+
29
+ def _plan(*segments: FlowSegment) -> FlowPlan:
30
+ return FlowPlan(language="en-US", flow=(FlowUnit(tuple(segments)),))
31
+
32
+
33
+ def _simple_inventory() -> BundleRoleInventory:
34
+ item = BundleRoleInventoryItem(
35
+ id="document-0001",
36
+ source="document.ssmd.md",
37
+ source_sha256="b" * 64,
38
+ plan="utterplan/document.utterplan.toml",
39
+ plan_id="sha256:" + "c" * 64,
40
+ explicit_roles=("host",),
41
+ explicit_occurrences={"host": 1},
42
+ uses_default_role=False,
43
+ implicit_occurrences=0,
44
+ )
45
+ return BundleRoleInventory(
46
+ bundle_kind="document",
47
+ manifest_sha256="a" * 64,
48
+ roles=(BundleRole("host", 1),),
49
+ uses_default_role=False,
50
+ implicit_occurrences=0,
51
+ items=(item,),
52
+ )
53
+
54
+
55
+ def _item(
56
+ bundle_root: Path,
57
+ item_id: str,
58
+ source_name: str,
59
+ plan_name: str,
60
+ plan: FlowPlan,
61
+ ) -> SimpleNamespace:
62
+ source = bundle_root / source_name
63
+ plan_path = bundle_root / plan_name
64
+ source.write_bytes(f"source for {item_id}".encode())
65
+ plan.save(plan_path, create_parent=True)
66
+ return SimpleNamespace(
67
+ id=item_id,
68
+ source_path=source,
69
+ source_relative_path=source_name,
70
+ plan_path=plan_path,
71
+ plan_relative_path=plan_name,
72
+ )
73
+
74
+
75
+ def test_builder_aggregates_compiled_roles_in_manifest_order_and_hashes_exact_files(
76
+ tmp_path: Path,
77
+ ) -> None:
78
+ manifest = tmp_path / "manifest.json"
79
+ manifest.write_bytes(b'{ "bundle_kind" : "book" }\n')
80
+ first = _item(
81
+ tmp_path,
82
+ "chapter-0001",
83
+ "chapter-1.ssmd.md",
84
+ "plans/chapter-1.toml",
85
+ _plan(
86
+ FlowSegment(
87
+ "Narration.",
88
+ "en-US",
89
+ directives=SegmentDirectives(voice=VoiceDirective("narrator")),
90
+ ),
91
+ FlowSegment("Default speech.", "en-US"),
92
+ FlowSegment("", "en-US", directives=SegmentDirectives(voice=VoiceDirective("empty"))),
93
+ FlowSegment(
94
+ "Music",
95
+ "en-US",
96
+ directives=SegmentDirectives(audio=AudioDirective(src="music.wav")),
97
+ ),
98
+ ),
99
+ )
100
+ second = _item(
101
+ tmp_path,
102
+ "chapter-0002",
103
+ "chapter-2.ssmd.md",
104
+ "plans/chapter-2.toml",
105
+ _plan(
106
+ FlowSegment(
107
+ "Host.", "en-US", directives=SegmentDirectives(voice=VoiceDirective("host"))
108
+ ),
109
+ FlowSegment(
110
+ "Narration again.",
111
+ "en-US",
112
+ directives=SegmentDirectives(voice=VoiceDirective("narrator")),
113
+ ),
114
+ FlowSegment(
115
+ "Another host.", "en-US", directives=SegmentDirectives(voice=VoiceDirective("host"))
116
+ ),
117
+ ),
118
+ )
119
+
120
+ inventory = build_bundle_role_inventory(
121
+ bundle_root=tmp_path,
122
+ bundle_kind="book",
123
+ manifest_path=manifest,
124
+ compiled_items=(first, second),
125
+ )
126
+
127
+ assert inventory.bundle_kind == "book"
128
+ assert inventory.manifest_sha256 == sha256_file(manifest)
129
+ assert [role.name for role in inventory.roles] == ["narrator", "host"]
130
+ assert [role.occurrences for role in inventory.roles] == [2, 2]
131
+ assert inventory.uses_default_role is True
132
+ assert inventory.implicit_occurrences == 1
133
+ assert [item.id for item in inventory.items] == ["chapter-0001", "chapter-0002"]
134
+ assert inventory.items[0].source == "chapter-1.ssmd.md"
135
+ assert inventory.items[0].plan == "plans/chapter-1.toml"
136
+ assert inventory.items[0].source_sha256 == sha256_file(first.source_path)
137
+ assert inventory.items[0].plan_id == FlowPlan.load(first.plan_path).plan_id
138
+ assert inventory.items[0].explicit_roles == ("narrator",)
139
+ assert inventory.items[0].implicit_occurrences == 1
140
+ assert inventory.items[1].explicit_roles == ("host", "narrator")
141
+ with pytest.raises(TypeError):
142
+ inventory.items[0].explicit_occurrences["new"] = 1 # type: ignore[index]
143
+
144
+
145
+ def test_save_load_is_deterministic_and_round_trips(tmp_path: Path) -> None:
146
+ manifest = tmp_path / "manifest.json"
147
+ manifest.write_bytes(b"{}")
148
+ compiled = _item(
149
+ tmp_path,
150
+ "document-0001",
151
+ "document.ssmd.md",
152
+ "document.utterplan.toml",
153
+ _plan(
154
+ FlowSegment(
155
+ "Host.", "en-US", directives=SegmentDirectives(voice=VoiceDirective("host"))
156
+ ),
157
+ FlowSegment(
158
+ "Host again.", "en-US", directives=SegmentDirectives(voice=VoiceDirective("host"))
159
+ ),
160
+ ),
161
+ )
162
+ inventory = build_bundle_role_inventory(
163
+ bundle_root=tmp_path,
164
+ bundle_kind="document",
165
+ manifest_path=manifest,
166
+ compiled_items=(compiled,),
167
+ )
168
+ first = tmp_path / "first.toml"
169
+ second = tmp_path / "second.toml"
170
+
171
+ save_bundle_role_inventory(inventory, first)
172
+ save_bundle_role_inventory(inventory, second)
173
+
174
+ assert first.read_bytes() == second.read_bytes()
175
+ assert load_bundle_role_inventory(first) == inventory
176
+ assert 'format = "utterplan.bundle-role-inventory"' in first.read_text(encoding="utf-8")
177
+ assert 'name = "host"' in first.read_text(encoding="utf-8")
178
+
179
+
180
+ def test_loader_rejects_unsupported_version_and_inconsistent_aggregate(tmp_path: Path) -> None:
181
+ manifest = tmp_path / "manifest.json"
182
+ manifest.write_bytes(b"{}")
183
+ compiled = _item(
184
+ tmp_path,
185
+ "document-0001",
186
+ "document.ssmd.md",
187
+ "document.utterplan.toml",
188
+ _plan(
189
+ FlowSegment(
190
+ "Host.", "en-US", directives=SegmentDirectives(voice=VoiceDirective("host"))
191
+ )
192
+ ),
193
+ )
194
+ inventory = build_bundle_role_inventory(
195
+ bundle_root=tmp_path,
196
+ bundle_kind="document",
197
+ manifest_path=manifest,
198
+ compiled_items=(compiled,),
199
+ )
200
+ path = tmp_path / "inventory.toml"
201
+ save_bundle_role_inventory(inventory, path)
202
+ original = path.read_text(encoding="utf-8")
203
+
204
+ path.write_text(original.replace("version = 1", "version = 2", 1), encoding="utf-8")
205
+ with pytest.raises(BundleRoleInventoryError, match="version"):
206
+ load_bundle_role_inventory(path)
207
+
208
+ path.write_text(original.replace("occurrences = 1", "occurrences = 2", 1), encoding="utf-8")
209
+ with pytest.raises(BundleRoleInventoryError, match="aggregate role"):
210
+ load_bundle_role_inventory(path)
211
+
212
+
213
+ def test_models_reject_duplicate_roles_and_inconsistent_implicit_metadata() -> None:
214
+ with pytest.raises(BundleRoleInventoryError, match="duplicate aggregate role"):
215
+ BundleRoleInventory(
216
+ bundle_kind="document",
217
+ manifest_sha256="a" * 64,
218
+ roles=(BundleRole("host", 1), BundleRole("host", 1)),
219
+ uses_default_role=False,
220
+ implicit_occurrences=0,
221
+ items=(),
222
+ )
223
+
224
+
225
+ def test_inventory_contract_is_exported_from_package_root() -> None:
226
+ assert utterplan.BundleRole is BundleRole
227
+ assert utterplan.BundleRoleInventory is BundleRoleInventory
228
+ assert utterplan.BundleRoleInventoryError is BundleRoleInventoryError
229
+ assert utterplan.build_bundle_role_inventory is build_bundle_role_inventory
230
+ assert utterplan.load_bundle_role_inventory is load_bundle_role_inventory
231
+ assert utterplan.save_bundle_role_inventory is save_bundle_role_inventory
232
+
233
+
234
+ @pytest.mark.parametrize(
235
+ ("old", "new", "message"),
236
+ [
237
+ ('format = "utterplan.bundle-role-inventory"', 'format = "other"', "format"),
238
+ ('manifest_sha256 = "' + "a" * 64 + '"', 'manifest_sha256 = "bad"', "manifest_sha256"),
239
+ ('source_sha256 = "' + "b" * 64 + '"', 'source_sha256 = "bad"', "source_sha256"),
240
+ ("occurrences = 1", "occurrences = -1", "non-negative integer"),
241
+ ("occurrences = 1", "occurrences = true", "non-negative integer"),
242
+ ('explicit_roles = ["host"]', 'explicit_roles = ["host", "host"]', "duplicate"),
243
+ ('source = "document.ssmd.md"', 'source = "/tmp/document.ssmd.md"', "relative path"),
244
+ ("host = 1\n", "", "exactly the roles"),
245
+ ("uses_default_role = false", "uses_default_role = true", "uses_default_role"),
246
+ ('plan = "utterplan/document.utterplan.toml"\n', "", "missing"),
247
+ ],
248
+ )
249
+ def test_loader_rejects_malformed_or_inconsistent_contract(
250
+ tmp_path: Path, old: str, new: str, message: str
251
+ ) -> None:
252
+ path = tmp_path / "inventory.toml"
253
+ save_bundle_role_inventory(_simple_inventory(), path)
254
+ text = path.read_text(encoding="utf-8")
255
+ assert old in text
256
+ path.write_text(text.replace(old, new, 1), encoding="utf-8")
257
+
258
+ with pytest.raises(BundleRoleInventoryError, match=message):
259
+ load_bundle_role_inventory(path)
260
+
261
+
262
+ def test_loader_rejects_duplicate_item_ids() -> None:
263
+ inventory = _simple_inventory()
264
+ item = inventory.items[0]
265
+ with pytest.raises(BundleRoleInventoryError, match="duplicate bundle item id"):
266
+ BundleRoleInventory(
267
+ bundle_kind="document",
268
+ manifest_sha256=inventory.manifest_sha256,
269
+ roles=(BundleRole("host", 2),),
270
+ uses_default_role=False,
271
+ implicit_occurrences=0,
272
+ items=(item, item),
273
+ )
274
+
275
+
276
+ def test_uppercase_sha256_values_are_normalized() -> None:
277
+ item = BundleRoleInventoryItem(
278
+ id="document-0001",
279
+ source="document.ssmd.md",
280
+ source_sha256="B" * 64,
281
+ plan="document.utterplan.toml",
282
+ plan_id="sha256:" + "c" * 64,
283
+ explicit_roles=("host",),
284
+ explicit_occurrences={"host": 1},
285
+ uses_default_role=False,
286
+ implicit_occurrences=0,
287
+ )
288
+ inventory = BundleRoleInventory(
289
+ bundle_kind="document",
290
+ manifest_sha256="A" * 64,
291
+ roles=(BundleRole("host", 1),),
292
+ uses_default_role=False,
293
+ implicit_occurrences=0,
294
+ items=(item,),
295
+ )
296
+ assert inventory.manifest_sha256 == "a" * 64
297
+ assert inventory.items[0].source_sha256 == "b" * 64
298
+
299
+
300
+ def test_implicit_only_builder_does_not_invent_an_explicit_role(tmp_path: Path) -> None:
301
+ manifest = tmp_path / "manifest.json"
302
+ manifest.write_bytes(b"{}")
303
+ compiled = _item(
304
+ tmp_path,
305
+ "document-0001",
306
+ "document.ssmd.md",
307
+ "document.utterplan.toml",
308
+ _plan(
309
+ FlowSegment("Unassigned speech.", "en-US"),
310
+ FlowSegment("", "en-US", directives=SegmentDirectives(voice=VoiceDirective("empty"))),
311
+ FlowSegment(
312
+ "Audio only",
313
+ "en-US",
314
+ directives=SegmentDirectives(audio=AudioDirective(src="music.wav")),
315
+ ),
316
+ ),
317
+ )
318
+
319
+ inventory = build_bundle_role_inventory(
320
+ bundle_root=tmp_path,
321
+ bundle_kind="document",
322
+ manifest_path=manifest,
323
+ compiled_items=(compiled,),
324
+ )
325
+
326
+ assert inventory.roles == ()
327
+ assert inventory.uses_default_role is True
328
+ assert inventory.implicit_occurrences == 1
329
+ assert inventory.items[0].explicit_roles == ()
330
+
331
+
332
+ def test_manifest_and_source_content_hashes_change_when_bytes_change(tmp_path: Path) -> None:
333
+ manifest = tmp_path / "manifest.json"
334
+ manifest.write_bytes(b'{"version":1}')
335
+ compiled = _item(
336
+ tmp_path,
337
+ "document-0001",
338
+ "document.ssmd.md",
339
+ "document.utterplan.toml",
340
+ _plan(
341
+ FlowSegment(
342
+ "Host.", "en-US", directives=SegmentDirectives(voice=VoiceDirective("host"))
343
+ )
344
+ ),
345
+ )
346
+ first = build_bundle_role_inventory(
347
+ bundle_root=tmp_path,
348
+ bundle_kind="document",
349
+ manifest_path=manifest,
350
+ compiled_items=(compiled,),
351
+ )
352
+ manifest.write_bytes(b'{ "version": 1 }')
353
+ compiled.source_path.write_bytes(b"updated source bytes")
354
+ second = build_bundle_role_inventory(
355
+ bundle_root=tmp_path,
356
+ bundle_kind="document",
357
+ manifest_path=manifest,
358
+ compiled_items=(compiled,),
359
+ )
360
+
361
+ assert first.manifest_sha256 != second.manifest_sha256
362
+ assert first.items[0].source_sha256 != second.items[0].source_sha256
363
+ assert first.items[0].plan_id == second.items[0].plan_id
364
+
365
+
366
+ def test_save_failure_preserves_previous_inventory_atomically(
367
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
368
+ ) -> None:
369
+ path = tmp_path / "role-inventory.toml"
370
+ path.write_bytes(b"previous inventory")
371
+
372
+ def fail_replace(_source: object, _destination: object) -> None:
373
+ raise OSError("simulated replace failure")
374
+
375
+ monkeypatch.setattr("utterplan.atomic_io.os.replace", fail_replace)
376
+ with pytest.raises(OSError, match="simulated replace failure"):
377
+ save_bundle_role_inventory(_simple_inventory(), path)
378
+
379
+ assert path.read_bytes() == b"previous inventory"
380
+ assert list(tmp_path.iterdir()) == [path]
@@ -10,7 +10,9 @@ from renderplan import codec as renderplan_codec
10
10
 
11
11
  from utterplan._book_service import BookServiceError, compile_book, render_book
12
12
  from utterplan.bundle import BundleServiceError, compile_bundle, render_bundle
13
+ from utterplan.bundle_role_inventory import load_bundle_role_inventory, sha256_file
13
14
  from utterplan.config import LinguisticsConfig, PlannerConfig
15
+ from utterplan.model import FlowPlan
14
16
  from utterplan.ssmd_bundle import load_ssmd_bundle
15
17
 
16
18
  DOCUMENT_FIXTURE = Path(__file__).parent / "fixtures/document-bundle-v1/story.ssmd-bundle"
@@ -114,10 +116,73 @@ def test_compile_bundle_dispatches_both_kinds_and_returns_uniform_result(
114
116
  assert starts[0].kind == kind
115
117
  assert starts[0].selected_item_ids == result.selected_item_ids
116
118
  assert result.report_path.is_file()
119
+ assert result.role_inventory_path == result.output_dir / "role-inventory.toml"
120
+ assert result.role_inventory_path.is_file()
121
+ inventory = load_bundle_role_inventory(result.role_inventory_path)
122
+ assert inventory.bundle_kind == kind
123
+ assert [item.id for item in inventory.items] == list(result.selected_item_ids)
124
+ assert inventory.manifest_sha256 == sha256_file(root / "manifest.json")
125
+ for item in inventory.items:
126
+ assert item.source_sha256 == sha256_file(root / item.source)
127
+ assert item.plan_id == FlowPlan.load(root / item.plan).plan_id
117
128
 
118
129
  assert len(loaded_indexes) == 1
119
130
 
120
131
 
132
+ def test_partial_book_compile_leaves_canonical_inventory_absent_or_unchanged(
133
+ tmp_path: Path,
134
+ ) -> None:
135
+ partial_root = _book(tmp_path / "partial.ssmd-bundle")
136
+ partial = compile_bundle(partial_root, chapters="5", planner_config=_planner_config())
137
+ assert partial.exit_code == 0
138
+ assert partial.role_inventory_path is None
139
+ assert not (partial.output_dir / "role-inventory.toml").exists()
140
+
141
+ full_root = _book(tmp_path / "full-then-partial.ssmd-bundle")
142
+ full = compile_bundle(full_root, planner_config=_planner_config())
143
+ assert full.role_inventory_path is not None
144
+ previous = full.role_inventory_path.read_bytes()
145
+ subset = compile_bundle(full_root, chapters="5", planner_config=_planner_config(), force=True)
146
+ assert subset.exit_code == 0
147
+ assert subset.role_inventory_path is None
148
+ assert full.role_inventory_path.read_bytes() == previous
149
+
150
+
151
+ @pytest.mark.parametrize("preexisting", [False, True])
152
+ def test_failed_full_book_compile_does_not_replace_existing_inventory(
153
+ tmp_path: Path, preexisting: bool
154
+ ) -> None:
155
+ root = _book(tmp_path / "failed-full.ssmd-bundle")
156
+ output = root / "utterplan"
157
+ output.mkdir()
158
+ inventory_path = output / "role-inventory.toml"
159
+ previous = b"previous inventory"
160
+ if preexisting:
161
+ inventory_path.write_bytes(previous)
162
+ invalid_source = root / "chapters/chapter-0006.ssmd.md"
163
+ invalid_source.write_text("not valid SSMD", encoding="utf-8")
164
+
165
+ failed = compile_bundle(root, planner_config=_planner_config())
166
+
167
+ assert failed.exit_code == 1
168
+ assert failed.role_inventory_path is None
169
+ if preexisting:
170
+ assert inventory_path.read_bytes() == previous
171
+ else:
172
+ assert not inventory_path.exists()
173
+
174
+ invalid_source.write_text(
175
+ (root / "chapters/chapter-0005.ssmd.md").read_text(encoding="utf-8"),
176
+ encoding="utf-8",
177
+ )
178
+ recovered = compile_bundle(root, planner_config=_planner_config(), force=True)
179
+ assert recovered.exit_code == 0
180
+ assert recovered.role_inventory_path == inventory_path
181
+ assert load_bundle_role_inventory(inventory_path).bundle_kind == "book"
182
+ if preexisting:
183
+ assert inventory_path.read_bytes() != previous
184
+
185
+
121
186
  def test_document_bundle_only_accepts_all_chapters_for_compile_and_render(tmp_path: Path) -> None:
122
187
  root = _document(tmp_path / "story.ssmd-bundle")
123
188
 
@@ -284,7 +284,8 @@ def test_unselected_missing_or_malformed_chapter_does_not_block_selected_range(
284
284
  ["bundle", "compile", str(root), "--chapters", "5-6", "--text-preparation", "identity"]
285
285
  )
286
286
 
287
- capsys.readouterr()
287
+ captured = capsys.readouterr()
288
+ assert "role inventory:" not in captured.err
288
289
  assert result == 0
289
290
  assert (root / "utterplan/chapter-0005.utterplan.toml").exists()
290
291
  assert (root / "utterplan/chapter-0006.utterplan.toml").exists()
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  import pytest
8
8
  import tomlkit
9
9
 
10
+ from utterplan.bundle_role_inventory import load_bundle_role_inventory
10
11
  from utterplan.cli import build_parser, main
11
12
  from utterplan.model import FlowPlan
12
13
 
@@ -56,6 +57,14 @@ def test_compile_document_writes_default_layout_and_keeps_source_unchanged(
56
57
  assert "UtterPlan bundle compile" in captured.err
57
58
  assert f"[1/1] document-0001 {source}" in captured.err
58
59
  assert "written: 1" in captured.err
60
+ inventory_path = output_dir / "role-inventory.toml"
61
+ inventory = load_bundle_role_inventory(inventory_path)
62
+ assert (
63
+ f"roles: {', '.join(role.name for role in inventory.roles) or '(none)'}"
64
+ in captured.err
65
+ )
66
+ assert "default-role: required by " in captured.err
67
+ assert f"role inventory: {inventory_path}" in captured.err
59
68
  assert "Original content." in _plan_text(plan_path)
60
69
  report = _report(report_path)
61
70
  assert report["requested"] == 1
@@ -20,6 +20,15 @@ from .bundle_replan import (
20
20
  BundleReplanStartCallback,
21
21
  replan_bundle,
22
22
  )
23
+ from .bundle_role_inventory import (
24
+ BundleRole,
25
+ BundleRoleInventory,
26
+ BundleRoleInventoryError,
27
+ BundleRoleInventoryItem,
28
+ build_bundle_role_inventory,
29
+ load_bundle_role_inventory,
30
+ save_bundle_role_inventory,
31
+ )
23
32
  from .compiler import (
24
33
  CompileResult,
25
34
  InputFormat,
@@ -139,6 +148,13 @@ __all__ = [
139
148
  "BundleCompileResult",
140
149
  "BundleCompileStart",
141
150
  "BundleCompileStartCallback",
151
+ "BundleRole",
152
+ "BundleRoleInventory",
153
+ "BundleRoleInventoryError",
154
+ "BundleRoleInventoryItem",
155
+ "build_bundle_role_inventory",
156
+ "load_bundle_role_inventory",
157
+ "save_bundle_role_inventory",
142
158
  "BundleReplanError",
143
159
  "BundleReplanResult",
144
160
  "BundleReplanStart",
@@ -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.7'
22
- __version_tuple__ = version_tuple = (0, 6, 7)
21
+ __version__ = version = '0.6.8'
22
+ __version_tuple__ = version_tuple = (0, 6, 8)
23
23
 
24
- __commit_id__ = commit_id = 'g5282696da'
24
+ __commit_id__ = commit_id = 'g167978804'