utterplan 0.3.3__tar.gz → 0.3.4__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 (249) hide show
  1. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/events/events.jsonl +4 -0
  2. utterplan-0.3.4/.ledger/releaseledger/data/ledgers/main/releases/0.3.4/audit/commit-audit.yaml +13 -0
  3. utterplan-0.3.4/.ledger/releaseledger/data/ledgers/main/releases/0.3.4/entries/entry-0001.md +34 -0
  4. utterplan-0.3.4/.ledger/releaseledger/data/ledgers/main/releases/0.3.4/release.md +27 -0
  5. utterplan-0.3.4/.ledger/releaseledger/work/0.3.4/audit-decisions.yaml +1 -0
  6. utterplan-0.3.4/.ledger/releaseledger/work/0.3.4/audit.yaml +13 -0
  7. utterplan-0.3.4/.ledger/releaseledger/work/0.3.4/entries.preview.yaml +11 -0
  8. utterplan-0.3.4/.ledger/releaseledger/work/0.3.4/entries.yaml +11 -0
  9. utterplan-0.3.4/.ledger/releaseledger/work/0.3.4/evidence/manifest.json +12 -0
  10. utterplan-0.3.4/.ledger/releaseledger/work/0.3.4/range.json +13 -0
  11. {utterplan-0.3.3 → utterplan-0.3.4}/PKG-INFO +17 -1
  12. {utterplan-0.3.3 → utterplan-0.3.4}/README.md +16 -0
  13. {utterplan-0.3.3 → utterplan-0.3.4}/docs/changelog.md +8 -0
  14. {utterplan-0.3.3 → utterplan-0.3.4}/docs/python-api.md +27 -0
  15. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_linguistics.py +69 -1
  16. utterplan-0.3.4/tests/test_progress.py +231 -0
  17. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/__init__.py +5 -0
  18. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/_version.py +3 -3
  19. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/compiler.py +4 -2
  20. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/linguistics.py +105 -7
  21. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/planner.py +184 -8
  22. utterplan-0.3.4/utterplan/progress.py +60 -0
  23. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan.egg-info/PKG-INFO +17 -1
  24. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan.egg-info/SOURCES.txt +11 -0
  25. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan.egg-info/scm_file_list.json +11 -0
  26. utterplan-0.3.4/utterplan.egg-info/scm_version.json +8 -0
  27. utterplan-0.3.3/utterplan.egg-info/scm_version.json +0 -8
  28. {utterplan-0.3.3 → utterplan-0.3.4}/.codecrate.toml +0 -0
  29. {utterplan-0.3.3 → utterplan-0.3.4}/.github/workflows/codecov.yml +0 -0
  30. {utterplan-0.3.3 → utterplan-0.3.4}/.github/workflows/pre-commit.yml +0 -0
  31. {utterplan-0.3.3 → utterplan-0.3.4}/.github/workflows/python-publish.yml +0 -0
  32. {utterplan-0.3.3 → utterplan-0.3.4}/.github/workflows/tests.yml +0 -0
  33. {utterplan-0.3.3 → utterplan-0.3.4}/.gitignore +0 -0
  34. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/ledger.toml +0 -0
  35. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/.ledger-project.toml +0 -0
  36. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/config.toml +0 -0
  37. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/.ledger-project.toml +0 -0
  38. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.0/entries/entry-0001.md +0 -0
  39. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.0/entries/entry-0002.md +0 -0
  40. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.0/entries/entry-0003.md +0 -0
  41. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.0/entries/entry-0004.md +0 -0
  42. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.0/release.md +0 -0
  43. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.1/entries/entry-0001.md +0 -0
  44. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.1/release.md +0 -0
  45. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/audit/commit-audit.yaml +0 -0
  46. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/entries/entry-0001.md +0 -0
  47. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/entries/entry-0002.md +0 -0
  48. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/entries/entry-0003.md +0 -0
  49. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/entries/entry-0004.md +0 -0
  50. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/entries/entry-0005.md +0 -0
  51. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/entries/entry-0006.md +0 -0
  52. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.1.3/release.md +0 -0
  53. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.2.0/audit/commit-audit.yaml +0 -0
  54. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.2.0/entries/entry-0001.md +0 -0
  55. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.2.0/entries/entry-0002.md +0 -0
  56. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.2.0/entries/entry-0003.md +0 -0
  57. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.2.0/entries/entry-0004.md +0 -0
  58. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.2.0/release.md +0 -0
  59. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/audit/commit-audit.yaml +0 -0
  60. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/entries/entry-0001.md +0 -0
  61. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/entries/entry-0002.md +0 -0
  62. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/entries/entry-0003.md +0 -0
  63. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/entries/entry-0004.md +0 -0
  64. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/entries/entry-0005.md +0 -0
  65. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/entries/entry-0006.md +0 -0
  66. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.0/release.md +0 -0
  67. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.1/audit/commit-audit.yaml +0 -0
  68. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.1/entries/entry-0001.md +0 -0
  69. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.1/release.md +0 -0
  70. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.2/audit/commit-audit.yaml +0 -0
  71. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.2/entries/entry-0001.md +0 -0
  72. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.2/entries/entry-0002.md +0 -0
  73. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.2/release.md +0 -0
  74. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.3/audit/commit-audit.yaml +0 -0
  75. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.3/entries/entry-0001.md +0 -0
  76. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.3/entries/entry-0002.md +0 -0
  77. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/data/ledgers/main/releases/0.3.3/release.md +0 -0
  78. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/audit-decisions.yaml +0 -0
  79. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/audit.yaml +0 -0
  80. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/entries.yaml +0 -0
  81. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/evidence/2c64e29.patch +0 -0
  82. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/evidence/4022753.patch +0 -0
  83. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/evidence/66a88b6.patch +0 -0
  84. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/evidence/manifest.json +0 -0
  85. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.1.2/range.json +0 -0
  86. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/audit-decisions.yaml +0 -0
  87. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/audit.yaml +0 -0
  88. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/entries.yaml +0 -0
  89. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/evidence/9637182.patch +0 -0
  90. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/evidence/c52aa68.patch +0 -0
  91. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/evidence/e49a6dd.patch +0 -0
  92. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/evidence/manifest.json +0 -0
  93. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.2.0/range.json +0 -0
  94. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.0/audit-decisions.yaml +0 -0
  95. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.0/audit.yaml +0 -0
  96. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.0/entries.yaml +0 -0
  97. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.0/evidence/49023f0.patch +0 -0
  98. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.0/evidence/manifest.json +0 -0
  99. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.0/range.json +0 -0
  100. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.1/audit-decisions.yaml +0 -0
  101. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.1/audit.yaml +0 -0
  102. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.1/entries.yaml +0 -0
  103. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.1/evidence/70adac0.patch +0 -0
  104. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.1/evidence/manifest.json +0 -0
  105. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.1/range.json +0 -0
  106. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/audit-decisions.yaml +0 -0
  107. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/audit.yaml +0 -0
  108. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/entries.yaml +0 -0
  109. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/evidence/6c2a1ac.patch +0 -0
  110. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/evidence/a2bc20d.patch +0 -0
  111. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/evidence/ba3b5af.patch +0 -0
  112. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/evidence/manifest.json +0 -0
  113. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.2/range.json +0 -0
  114. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.3/audit-decisions.yaml +0 -0
  115. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.3/audit.yaml +0 -0
  116. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.3/entries.yaml +0 -0
  117. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.3/evidence/66e2d83.patch +0 -0
  118. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.3/evidence/ac797e4.patch +0 -0
  119. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.3/evidence/manifest.json +0 -0
  120. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/work/0.3.3/range.json +0 -0
  121. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/releaseledger/write.lock +0 -0
  122. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/taskledger/.ledger-project.toml +0 -0
  123. {utterplan-0.3.3 → utterplan-0.3.4}/.ledger/taskledger/config.toml +0 -0
  124. {utterplan-0.3.3 → utterplan-0.3.4}/.pre-commit-config.yaml +0 -0
  125. {utterplan-0.3.3 → utterplan-0.3.4}/AGENTS.md +0 -0
  126. {utterplan-0.3.3 → utterplan-0.3.4}/LICENSE +0 -0
  127. {utterplan-0.3.3 → utterplan-0.3.4}/NOTICE +0 -0
  128. {utterplan-0.3.3 → utterplan-0.3.4}/docs/Makefile +0 -0
  129. {utterplan-0.3.3 → utterplan-0.3.4}/docs/architecture.md +0 -0
  130. {utterplan-0.3.3 → utterplan-0.3.4}/docs/cli.md +0 -0
  131. {utterplan-0.3.3 → utterplan-0.3.4}/docs/conf.py +0 -0
  132. {utterplan-0.3.3 → utterplan-0.3.4}/docs/consumer-guide.md +0 -0
  133. {utterplan-0.3.3 → utterplan-0.3.4}/docs/coordinate-spaces.md +0 -0
  134. {utterplan-0.3.3 → utterplan-0.3.4}/docs/debugging.md +0 -0
  135. {utterplan-0.3.3 → utterplan-0.3.4}/docs/format.md +0 -0
  136. {utterplan-0.3.3 → utterplan-0.3.4}/docs/getting-started.md +0 -0
  137. {utterplan-0.3.3 → utterplan-0.3.4}/docs/index.md +0 -0
  138. {utterplan-0.3.3 → utterplan-0.3.4}/docs/make.bat +0 -0
  139. {utterplan-0.3.3 → utterplan-0.3.4}/docs/phase-a-completion.md +0 -0
  140. {utterplan-0.3.3 → utterplan-0.3.4}/docs/pykokoro-integration.md +0 -0
  141. {utterplan-0.3.3 → utterplan-0.3.4}/docs/requirements.txt +0 -0
  142. {utterplan-0.3.3 → utterplan-0.3.4}/examples/basic.py +0 -0
  143. {utterplan-0.3.3 → utterplan-0.3.4}/examples/chapter.ssmd +0 -0
  144. {utterplan-0.3.3 → utterplan-0.3.4}/examples/compile_file.py +0 -0
  145. {utterplan-0.3.3 → utterplan-0.3.4}/examples/inspect_plan.py +0 -0
  146. {utterplan-0.3.3 → utterplan-0.3.4}/examples/multilingual.py +0 -0
  147. {utterplan-0.3.3 → utterplan-0.3.4}/examples/ssmd_example.py +0 -0
  148. {utterplan-0.3.3 → utterplan-0.3.4}/pyproject.toml +0 -0
  149. {utterplan-0.3.3 → utterplan-0.3.4}/setup.cfg +0 -0
  150. {utterplan-0.3.3 → utterplan-0.3.4}/spec/schemas/v1.schema.json +0 -0
  151. {utterplan-0.3.3 → utterplan-0.3.4}/spec/schemas/v2.schema.json +0 -0
  152. {utterplan-0.3.3 → utterplan-0.3.4}/spec/schemas/v3.schema.json +0 -0
  153. {utterplan-0.3.3 → utterplan-0.3.4}/spec/utterplan.schema.json +0 -0
  154. {utterplan-0.3.3 → utterplan-0.3.4}/tests/__init__.py +0 -0
  155. {utterplan-0.3.3 → utterplan-0.3.4}/tests/fixtures/canonical_consumer_contract.expected.json +0 -0
  156. {utterplan-0.3.3 → utterplan-0.3.4}/tests/fixtures/canonical_consumer_contract.ssmd +0 -0
  157. {utterplan-0.3.3 → utterplan-0.3.4}/tests/fixtures/ssmd_09_comprehensive.ssmd +0 -0
  158. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/basic_en.utterplan.json +0 -0
  159. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/directives.utterplan.json +0 -0
  160. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/markers.utterplan.json +0 -0
  161. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/multilingual.utterplan.json +0 -0
  162. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/parenthetical.utterplan.json +0 -0
  163. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/spokenform_offsets.utterplan.json +0 -0
  164. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/ssmd_09_comprehensive.utterplan.json +0 -0
  165. {utterplan-0.3.3 → utterplan-0.3.4}/tests/golden/ssmd_breaks.utterplan.json +0 -0
  166. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/__init__.py +0 -0
  167. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/cases.py +0 -0
  168. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/goldens/multilingual_preparation.json +0 -0
  169. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/goldens/spokenform_numbers.json +0 -0
  170. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/goldens/ssmd_break.json +0 -0
  171. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/goldens/ssmd_language_detection.json +0 -0
  172. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/goldens/ssmd_marker.json +0 -0
  173. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/goldens/ssmd_pronunciation.json +0 -0
  174. {utterplan-0.3.3 → utterplan-0.3.4}/tests/migration/test_semantic_migration_regressions.py +0 -0
  175. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/README.md +0 -0
  176. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v1/basic.json +0 -0
  177. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v1/directives.json +0 -0
  178. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v1/markers.json +0 -0
  179. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v1/multilingual.json +0 -0
  180. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v1/parenthetical.json +0 -0
  181. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v1/spokenform_offsets.json +0 -0
  182. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v1/ssmd_breaks.json +0 -0
  183. {utterplan-0.3.3 → utterplan-0.3.4}/tests/schema_history/v2/directives.json +0 -0
  184. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_boundaries.py +0 -0
  185. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_cli.py +0 -0
  186. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_compiler.py +0 -0
  187. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_consumer_contract.py +0 -0
  188. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_consumer_readiness.py +0 -0
  189. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_coordinates.py +0 -0
  190. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_directives.py +0 -0
  191. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_explain.py +0 -0
  192. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_fake_renderer.py +0 -0
  193. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_format.py +0 -0
  194. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_golden.py +0 -0
  195. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_identity_units.py +0 -0
  196. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_import_boundaries.py +0 -0
  197. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_language.py +0 -0
  198. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_migration_framework.py +0 -0
  199. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_pauses.py +0 -0
  200. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_planner.py +0 -0
  201. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_preparation.py +0 -0
  202. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_public_identity.py +0 -0
  203. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_schema_history.py +0 -0
  204. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_schema_registry.py +0 -0
  205. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_schema_strict.py +0 -0
  206. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_ssmd_09_parser.py +0 -0
  207. {utterplan-0.3.3 → utterplan-0.3.4}/tests/test_version.py +0 -0
  208. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/__main__.py +0 -0
  209. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/cli.py +0 -0
  210. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/config.py +0 -0
  211. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/diagnostics.py +0 -0
  212. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/directives.py +0 -0
  213. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/exceptions.py +0 -0
  214. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/explain.py +0 -0
  215. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/format.py +0 -0
  216. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/hashing.py +0 -0
  217. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/language.py +0 -0
  218. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/migration.py +0 -0
  219. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/migrations/__init__.py +0 -0
  220. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/migrations/registry.py +0 -0
  221. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/migrations/v1_to_v2.py +0 -0
  222. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/migrations/v2_to_v3.py +0 -0
  223. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/model.py +0 -0
  224. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/parsers.py +0 -0
  225. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/pauses.py +0 -0
  226. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/preparation.py +0 -0
  227. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/py.typed +0 -0
  228. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/schema_registry.py +0 -0
  229. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/schemas/v1.schema.json +0 -0
  230. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/schemas/v2.schema.json +0 -0
  231. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/schemas/v3.schema.json +0 -0
  232. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/serialization.py +0 -0
  233. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/spacy_models.py +0 -0
  234. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/spans.py +0 -0
  235. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/__init__.py +0 -0
  236. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/document/__init__.py +0 -0
  237. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/document/plain.py +0 -0
  238. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/document/ssmd.py +0 -0
  239. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/preparation/__init__.py +0 -0
  240. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/preparation/identity.py +0 -0
  241. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/preparation/spokenform.py +0 -0
  242. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/stages/protocols.py +0 -0
  243. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/units.py +0 -0
  244. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/utterplan.schema.json +0 -0
  245. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan/versioning.py +0 -0
  246. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan.egg-info/dependency_links.txt +0 -0
  247. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan.egg-info/entry_points.txt +0 -0
  248. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan.egg-info/requires.txt +0 -0
  249. {utterplan-0.3.3 → utterplan-0.3.4}/utterplan.egg-info/top_level.txt +0 -0
@@ -60,3 +60,7 @@
60
60
  {"data":{"updated_rows":2},"event":"audit.updated","event_id":"event-0060","record_revisions":{"commit_audit_sheet":2},"release_version":"0.3.3","schema_version":2}
61
61
  {"data":{"entry_ids":["entry-0001","entry-0002"],"synced_audit_rows":2},"event":"entry.batch_added","event_id":"event-0061","record_revisions":{"commit_audit_sheet":3,"entry:0.3.3/entry-0001":1,"entry:0.3.3/entry-0002":1,"release:0.3.3":3},"release_version":"0.3.3","schema_version":2}
62
62
  {"data":{"released_at":"2026-10-03"},"event":"release.finalized","event_id":"event-0062","record_revisions":{"release:0.3.3":4},"release_version":"0.3.3","schema_version":2}
63
+ {"data":{"status":"planned"},"event":"release.created","event_id":"event-0063","record_revisions":{"release:0.3.4":1},"release_version":"0.3.4","schema_version":2}
64
+ {"data":{"fields":["git_base_ref","git_base_sha","git_commit_count","git_head_ref","git_head_sha","git_range"]},"event":"release.updated","event_id":"event-0064","record_revisions":{"release:0.3.4":2},"release_version":"0.3.4","schema_version":2}
65
+ {"data":{"row_count":0},"event":"audit.created","event_id":"event-0065","record_revisions":{"commit_audit_sheet":1},"release_version":"0.3.4","schema_version":2}
66
+ {"data":{"kind":"added","status":"accepted"},"entry_id":"entry-0001","event":"entry.added","event_id":"event-0066","record_revisions":{"entry:0.3.4/entry-0001":1,"release:0.3.4":3},"release_version":"0.3.4","schema_version":2}
@@ -0,0 +1,13 @@
1
+ schema_version: 1
2
+ object_type: commit_audit_sheet
3
+ versioning:
4
+ schema_version: 1
5
+ revision: 1
6
+ release_version: 0.3.4
7
+ git_base_ref: v0.3.3
8
+ git_base_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
9
+ git_head_ref: HEAD
10
+ git_head_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
11
+ git_range: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158..0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
12
+ commit_count: 0
13
+ rows: []
@@ -0,0 +1,34 @@
1
+ ---
2
+ schema_version: 2
3
+ object_type: release_entry
4
+ versioning:
5
+ schema_version: 1
6
+ revision: 1
7
+ entry_id: entry-0001
8
+ release_version: 0.3.4
9
+ kind: added
10
+ summary: Added typed planner progress events and reused unchanged linguistic analyses
11
+ status: accepted
12
+ audience: null
13
+ scopes: []
14
+ source_refs:
15
+ - tl:task-0021
16
+ paths:
17
+ - utterplan/progress.py
18
+ - utterplan/planner.py
19
+ - utterplan/linguistics.py
20
+ - utterplan/compiler.py
21
+ - tests/test_progress.py
22
+ - tests/test_linguistics.py
23
+ - docs/python-api.md
24
+ - README.md
25
+ issues: []
26
+ prs: []
27
+ sources: []
28
+ contributors: []
29
+ breaking: false
30
+ internal: false
31
+ order: 1
32
+ ---
33
+
34
+ The optional synchronous callback reports planner phases, provider runs, and actual spaCy model loads without changing plan serialization; equivalent analyses are reused where safe.
@@ -0,0 +1,27 @@
1
+ ---
2
+ schema_version: 2
3
+ object_type: release
4
+ versioning:
5
+ schema_version: 1
6
+ revision: 3
7
+ version: 0.3.4
8
+ status: planned
9
+ history_state: curated
10
+ title: null
11
+ released_at: null
12
+ previous_version: 0.3.3
13
+ cancel_reason: null
14
+ superseded_by: null
15
+ changelog_file: null
16
+ boundary_ref: null
17
+ source_refs: []
18
+ source_count: null
19
+ entry_count: 1
20
+ artifact_count: 0
21
+ git_base_ref: v0.3.3
22
+ git_base_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
23
+ git_head_ref: HEAD
24
+ git_head_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
25
+ git_range: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158..0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
26
+ git_commit_count: 0
27
+ ---
@@ -0,0 +1,13 @@
1
+ schema_version: 1
2
+ object_type: commit_audit_sheet
3
+ versioning:
4
+ schema_version: 1
5
+ revision: 1
6
+ release_version: 0.3.4
7
+ git_base_ref: v0.3.3
8
+ git_base_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
9
+ git_head_ref: HEAD
10
+ git_head_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
11
+ git_range: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158..0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
12
+ commit_count: 0
13
+ rows: []
@@ -0,0 +1,11 @@
1
+ schema: releaseledger.entry-batch.v1
2
+ operation: create
3
+ schema_version: 1
4
+ object_type: release_entry_batch
5
+ release_version: 0.3.4
6
+ git_base_ref: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
7
+ git_base_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
8
+ git_head_ref: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
9
+ git_head_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
10
+ include_merges: nontrivial
11
+ entries: []
@@ -0,0 +1,11 @@
1
+ schema: releaseledger.entry-batch.v1
2
+ operation: create
3
+ schema_version: 1
4
+ object_type: release_entry_batch
5
+ release_version: 0.3.4
6
+ git_base_ref: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
7
+ git_base_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
8
+ git_head_ref: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
9
+ git_head_sha: 0e4e0751e2b8a5d5c0592a3efbda5a31f653c158
10
+ include_merges: nontrivial
11
+ entries: []
@@ -0,0 +1,12 @@
1
+ {
2
+ "candidate_count": 0,
3
+ "candidates": [],
4
+ "git_base_ref": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
5
+ "git_base_sha": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
6
+ "git_head_ref": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
7
+ "git_head_sha": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
8
+ "include_merges": "nontrivial",
9
+ "object_type": "git_evidence_manifest",
10
+ "release_version": "0.3.4",
11
+ "schema_version": 1
12
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "base_ref": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
3
+ "base_sha": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
4
+ "candidate_count": 0,
5
+ "commit_count": 0,
6
+ "head_ref": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
7
+ "head_sha": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158",
8
+ "include_merges": "nontrivial",
9
+ "kind": "git_range_summary",
10
+ "merge_commit_count": 0,
11
+ "merge_commits_skipped": 0,
12
+ "range": "0e4e0751e2b8a5d5c0592a3efbda5a31f653c158..0e4e0751e2b8a5d5c0592a3efbda5a31f653c158"
13
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utterplan
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
@@ -343,6 +343,22 @@ identity. Plain text must be explicitly selected in Python and always requires a
343
343
  language fallback. Existing `UtterancePlanner.plan` remains supported and returns
344
344
  only the plan; JSON is the portable persistence and interchange format.
345
345
 
346
+ For operational progress from a long-running Python plan, pass `on_progress` to
347
+ `UtterancePlanner.plan`, `UtterancePlanner.compile`, or `compile_document`. The
348
+ callback receives typed `PlannerProgressEvent` objects and does not change the plan:
349
+
350
+ ```python
351
+ from utterplan import PlannerConfig, UtterancePlanner
352
+
353
+ events = []
354
+ plan = UtterancePlanner(PlannerConfig(language="en-us")).plan(
355
+ text,
356
+ on_progress=events.append,
357
+ )
358
+ ```
359
+
360
+ Callbacks run synchronously and exceptions propagate. See the [Python API guide](docs/python-api.md#planner-progress-callbacks) for event fields, model-load events, and reuse behavior.
361
+
346
362
  ## Renderer-consumer boundary
347
363
 
348
364
  Renderers consume `PlanSegment.text`, which is prepared/spoken text, and use
@@ -101,6 +101,22 @@ identity. Plain text must be explicitly selected in Python and always requires a
101
101
  language fallback. Existing `UtterancePlanner.plan` remains supported and returns
102
102
  only the plan; JSON is the portable persistence and interchange format.
103
103
 
104
+ For operational progress from a long-running Python plan, pass `on_progress` to
105
+ `UtterancePlanner.plan`, `UtterancePlanner.compile`, or `compile_document`. The
106
+ callback receives typed `PlannerProgressEvent` objects and does not change the plan:
107
+
108
+ ```python
109
+ from utterplan import PlannerConfig, UtterancePlanner
110
+
111
+ events = []
112
+ plan = UtterancePlanner(PlannerConfig(language="en-us")).plan(
113
+ text,
114
+ on_progress=events.append,
115
+ )
116
+ ```
117
+
118
+ Callbacks run synchronously and exceptions propagate. See the [Python API guide](docs/python-api.md#planner-progress-callbacks) for event fields, model-load events, and reuse behavior.
119
+
104
120
  ## Renderer-consumer boundary
105
121
 
106
122
  Renderers consume `PlanSegment.text`, which is prepared/spoken text, and use
@@ -4,6 +4,14 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.4] - Unreleased
8
+
9
+ ### Added
10
+
11
+ - Added typed planner progress events and reused unchanged linguistic analyses
12
+
13
+ <!-- generated by releaseledger -->
14
+
7
15
  ## [0.3.3] - 2026-10-03
8
16
 
9
17
  ### Added
@@ -41,6 +41,33 @@ the plan, is not renderer input, and does not change `plan.plan_id`. Its source
41
41
  ranges index SSMD-clean structural text; transformation output ranges index prepared
42
42
  spoken text, using Python string character offsets.
43
43
 
44
+ ## Planner progress callbacks
45
+
46
+ `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.
47
+
48
+ ```python
49
+ from utterplan import PlannerConfig, PlannerProgressEvent, UtterancePlanner
50
+
51
+
52
+ def report(event: PlannerProgressEvent) -> None:
53
+ if event.kind == "run.started":
54
+ print(
55
+ event.phase,
56
+ event.pass_index,
57
+ event.language,
58
+ event.provider,
59
+ event.model,
60
+ )
61
+
62
+
63
+ planner = UtterancePlanner(PlannerConfig(language="en-us"))
64
+ plan = planner.plan(text, on_progress=report)
65
+ ```
66
+
67
+ The phase events cover parsing, source analysis, preparation, spoken analysis, segmentation, and finalization. Each provider run reports `run.started` and `run.completed`; `completed` counts fully completed runs and `total` is the number of runs in that phase. When a local spaCy model is actually loaded, `model.started` and `model.completed` identify its language, provider, and resolved model. Cached models produce no load events. Events contain primitive metadata only, never spaCy pipelines or documents.
68
+
69
+ Callbacks should be lightweight. An exception raised by a callback propagates to the caller and can be used to stop planning. A provider invocation such as `pipeline(text)` is one opaque work unit: events report its start and completion but do not claim smooth percentages or internal progress. If spoken-text preparation leaves the linguistic input unchanged, the planner can reuse source analysis; the `spoken_analysis` phase events expose `details["reused"] = True` and no provider-run events are emitted for that reuse. Identity preparation also reports `source_analysis` as skipped (`details["skipped"] = True`) because it does not use source analyses; the single actual provider pass runs as logical pass 2.
70
+
44
71
  ## SSMD input and semantic plan
45
72
 
46
73
  The SSMD parser accepts dialect 0.9 only. Select SSMD explicitly for unversioned canonical fragments with `PlannerConfig(document_format="ssmd")`. Older SSMD source must be migrated with `ssmd migrate FILE --to 0.9`; `migrate_plan_data` and `utterplan migrate` apply to serialized UtterPlan JSON, not source documents.
@@ -197,7 +197,7 @@ def test_clausal_boundaries_reuse_provider_document(monkeypatch):
197
197
 
198
198
  plan = UtterancePlanner(config).plan(source)
199
199
 
200
- assert len(docs) == 2
200
+ assert len(docs) == 1
201
201
  assert detected == [(source, docs[-1])]
202
202
  boundary = next(item for item in plan.boundaries if item.kind == "clausal_comma")
203
203
  assert boundary.position == source.index(",")
@@ -311,3 +311,71 @@ def test_token_semantics_change_unit_hash_but_provenance_does_not(monkeypatch):
311
311
 
312
312
  metadata_only = replace(left, linguistic_runs=(changed_provenance,))
313
313
  assert metadata_only.units[0].content_hash == left.units[0].content_hash
314
+
315
+
316
+ def _count_analysis_calls(monkeypatch: pytest.MonkeyPatch) -> list[str]:
317
+ calls: list[str] = []
318
+ original = LinguisticResourcePool.analyze
319
+
320
+ def counted(self, text, run, config, *, phase="source_analysis", on_progress=None):
321
+ calls.append(text)
322
+ return original(self, text, run, config, phase=phase, on_progress=on_progress)
323
+
324
+ monkeypatch.setattr(LinguisticResourcePool, "analyze", counted)
325
+ return calls
326
+
327
+
328
+ def test_identity_preparation_analyzes_once_and_preserves_plan(
329
+ monkeypatch: pytest.MonkeyPatch,
330
+ ) -> None:
331
+ calls = _count_analysis_calls(monkeypatch)
332
+ config = PlannerConfig(language="en-us", document_format="plain", text_preparation="identity")
333
+ baseline = UtterancePlanner(config).plan("Hello world.")
334
+ assert len(calls) == 1
335
+ calls.clear()
336
+
337
+ events = []
338
+ observed = UtterancePlanner(config).plan("Hello world.", on_progress=events.append)
339
+
340
+ assert len(calls) == 1
341
+ assert observed.plan_id == baseline.plan_id
342
+ assert observed.to_json() == baseline.to_json()
343
+ assert observed.document_metadata["planning"]["linguistic_passes"] == 2
344
+ skipped = next(
345
+ event
346
+ for event in events
347
+ if event.kind == "phase.started" and event.phase == "source_analysis"
348
+ )
349
+ assert skipped.details["skipped"] is True
350
+ assert [event.pass_index for event in events if event.kind == "run.started"] == [2]
351
+
352
+
353
+ def test_unchanged_spokenform_reuses_source_analysis(monkeypatch: pytest.MonkeyPatch) -> None:
354
+ calls = _count_analysis_calls(monkeypatch)
355
+ events = []
356
+ plan = UtterancePlanner(
357
+ PlannerConfig(language="en-us", document_format="plain", text_preparation="spokenform")
358
+ ).plan("Hello world.", on_progress=events.append)
359
+
360
+ assert plan.texts.spoken == "Hello world."
361
+ assert calls == ["Hello world."]
362
+ reused = [
363
+ event
364
+ for event in events
365
+ if event.phase == "spoken_analysis" and event.details.get("reused") is True
366
+ ]
367
+ assert [event.kind for event in reused] == ["phase.started", "phase.completed"]
368
+ assert not any(
369
+ event.kind == "run.started" and event.phase == "spoken_analysis" for event in events
370
+ )
371
+
372
+
373
+ def test_changed_spokenform_still_analyzes_prepared_text(monkeypatch: pytest.MonkeyPatch) -> None:
374
+ calls = _count_analysis_calls(monkeypatch)
375
+ source = "Dr. Smith lives here."
376
+ plan = UtterancePlanner(
377
+ PlannerConfig(language="en-us", document_format="plain", text_preparation="spokenform")
378
+ ).plan(source)
379
+
380
+ assert plan.texts.spoken == "Doctor Smith lives here."
381
+ assert calls == [source, plan.texts.spoken]
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import sys
5
+ from collections.abc import Mapping
6
+ from dataclasses import fields
7
+ from types import SimpleNamespace
8
+
9
+ import pytest
10
+
11
+ from utterplan import (
12
+ LinguisticsConfig,
13
+ PlannerConfig,
14
+ PlannerProgressEvent,
15
+ ProgressCallback,
16
+ UtterancePlanner,
17
+ compile_document,
18
+ )
19
+
20
+ TEXT = "Hello world."
21
+
22
+
23
+ def _identity_config(**kwargs: object) -> PlannerConfig:
24
+ return PlannerConfig(
25
+ language="en-us",
26
+ document_format="plain",
27
+ text_preparation="identity",
28
+ **kwargs,
29
+ )
30
+
31
+
32
+ def test_public_progress_callback_reports_major_phases_and_runs() -> None:
33
+ events: list[PlannerProgressEvent] = []
34
+ planner = UtterancePlanner(_identity_config())
35
+
36
+ plan = planner.plan(TEXT, on_progress=events.append)
37
+
38
+ phases = [event.phase for event in events if event.kind == "phase.started"]
39
+ assert phases == [
40
+ "parse",
41
+ "source_analysis",
42
+ "preparation",
43
+ "spoken_analysis",
44
+ "segmentation",
45
+ "finalization",
46
+ ]
47
+ for phase in phases:
48
+ assert (
49
+ sum(event.kind == "phase.completed" and event.phase == phase for event in events) == 1
50
+ )
51
+
52
+ run_events = [event for event in events if event.kind.startswith("run.")]
53
+ assert [(event.kind, event.phase, event.completed, event.total) for event in run_events] == [
54
+ ("run.started", "spoken_analysis", 0, 1),
55
+ ("run.completed", "spoken_analysis", 1, 1),
56
+ ]
57
+ skipped_source = next(
58
+ event
59
+ for event in events
60
+ if event.kind == "phase.started" and event.phase == "source_analysis"
61
+ )
62
+ assert skipped_source.details["skipped"] is True
63
+ assert all(event.pass_total == 2 for event in run_events)
64
+ assert all(event.language == "en-us" for event in run_events)
65
+ assert all(event.char_count == len(TEXT) for event in run_events)
66
+ assert all(event.provider == "fallback" and event.model is None for event in run_events)
67
+ assert plan.texts.spoken == TEXT
68
+
69
+
70
+ def test_compile_document_forwards_progress_callback() -> None:
71
+ events: list[PlannerProgressEvent] = []
72
+ result = compile_document(
73
+ TEXT,
74
+ input_format="plain",
75
+ config=_identity_config(),
76
+ on_progress=events.append,
77
+ )
78
+
79
+ assert result.plan.texts.spoken == TEXT
80
+ assert events
81
+ assert events[0].kind == "phase.started"
82
+ assert events[0].phase == "parse"
83
+
84
+
85
+ def test_planner_compile_forwards_progress_callback() -> None:
86
+ events: list[PlannerProgressEvent] = []
87
+ result = UtterancePlanner(_identity_config()).compile(TEXT, on_progress=events.append)
88
+
89
+ assert result.plan.texts.spoken == TEXT
90
+ assert any(event.phase == "finalization" for event in events)
91
+
92
+
93
+ def test_progress_callback_exceptions_propagate() -> None:
94
+ class MarkerError(RuntimeError):
95
+ pass
96
+
97
+ def callback(_event: PlannerProgressEvent) -> None:
98
+ raise MarkerError
99
+
100
+ with pytest.raises(MarkerError):
101
+ UtterancePlanner(_identity_config()).plan(TEXT, on_progress=callback)
102
+
103
+
104
+ def test_model_load_events_bracket_real_load_and_cached_calls(
105
+ monkeypatch: pytest.MonkeyPatch,
106
+ ) -> None:
107
+ timeline: list[str] = []
108
+
109
+ class Pipeline:
110
+ meta = {"version": "1.0"}
111
+
112
+ def __call__(self, text: str) -> list[SimpleNamespace]:
113
+ timeline.append("provider")
114
+ return [
115
+ SimpleNamespace(
116
+ idx=match.start(),
117
+ text=match.group(0),
118
+ pos_="NOUN",
119
+ tag_="NN",
120
+ lemma_=match.group(0).lower(),
121
+ morph="",
122
+ )
123
+ for match in re.finditer(r"\S+", text)
124
+ ]
125
+
126
+ pipeline = Pipeline()
127
+ loaded: list[str] = []
128
+
129
+ def load(model: str) -> Pipeline:
130
+ timeline.append("load")
131
+ loaded.append(model)
132
+ return pipeline
133
+
134
+ monkeypatch.setitem(
135
+ sys.modules,
136
+ "spacy",
137
+ SimpleNamespace(__version__="3.7.0", load=load),
138
+ )
139
+ config = _identity_config(
140
+ linguistics=LinguisticsConfig(use_spacy=True, spacy_model="fake_model", require_spacy=True)
141
+ )
142
+ events: list[PlannerProgressEvent] = []
143
+
144
+ def report(event: PlannerProgressEvent) -> None:
145
+ events.append(event)
146
+ timeline.append(event.kind)
147
+
148
+ planner = UtterancePlanner(config)
149
+ planner.plan(TEXT, on_progress=report)
150
+ planner.plan(TEXT, on_progress=report)
151
+
152
+ assert loaded == ["fake_model"]
153
+ assert [event.kind for event in events if event.kind.startswith("model.")] == [
154
+ "model.started",
155
+ "model.completed",
156
+ ]
157
+ model_events = [event for event in events if event.kind.startswith("model.")]
158
+ assert all(event.phase == "spoken_analysis" for event in model_events)
159
+ assert all(event.language == "en-us" for event in model_events)
160
+ assert all(event.provider == "spacy" and event.model == "fake_model" for event in model_events)
161
+ assert timeline.index("run.started") < timeline.index("model.started")
162
+ assert (
163
+ timeline.index("model.started") < timeline.index("load") < timeline.index("model.completed")
164
+ )
165
+ assert timeline.index("model.completed") < timeline.index("provider")
166
+ assert timeline.index("provider") < timeline.index("run.completed")
167
+
168
+
169
+ def test_progress_callback_type_is_public() -> None:
170
+ def callback(_event: PlannerProgressEvent) -> None:
171
+ return None
172
+
173
+ public_callback: ProgressCallback = callback
174
+ assert callable(public_callback)
175
+
176
+
177
+ def test_callback_output_matches_baseline_and_events_are_public_primitives() -> None:
178
+ source = "Dr. Smith lives here."
179
+ config = PlannerConfig(language="en-us", document_format="plain", text_preparation="spokenform")
180
+ baseline = UtterancePlanner(config).plan(source)
181
+ events: list[PlannerProgressEvent] = []
182
+ observed = UtterancePlanner(config).plan(source, on_progress=events.append)
183
+
184
+ assert observed.plan_id == baseline.plan_id
185
+ assert observed.to_json() == baseline.to_json()
186
+
187
+ def is_public_value(value: object) -> bool:
188
+ if value is None or isinstance(value, (str, int, float, bool)):
189
+ return True
190
+ if isinstance(value, Mapping):
191
+ return all(
192
+ isinstance(key, str) and is_public_value(item) for key, item in value.items()
193
+ )
194
+ if isinstance(value, (list, tuple)):
195
+ return all(is_public_value(item) for item in value)
196
+ return False
197
+
198
+ assert events
199
+ assert all(
200
+ is_public_value(getattr(event, field.name)) for event in events for field in fields(event)
201
+ )
202
+
203
+
204
+ def test_multilingual_run_progress_is_monotonic_and_matches_plan_runs() -> None:
205
+ source = (
206
+ '---\nssmd_version: "0.9"\nlanguage: en-us\n---\n'
207
+ 'Hello [bonjour]{lang="fr"} [hola]{lang="es"}.'
208
+ )
209
+ events: list[PlannerProgressEvent] = []
210
+ plan = UtterancePlanner(
211
+ PlannerConfig(language="en-us", document_format="ssmd", text_preparation="identity")
212
+ ).plan(source, on_progress=events.append)
213
+
214
+ started = [
215
+ event
216
+ for event in events
217
+ if event.kind == "run.started" and event.phase == "spoken_analysis"
218
+ ]
219
+ completed = [
220
+ event
221
+ for event in events
222
+ if event.kind == "run.completed" and event.phase == "spoken_analysis"
223
+ ]
224
+ expected_languages = [run.language for run in plan.languages]
225
+
226
+ assert [event.language for event in started] == expected_languages
227
+ assert [event.language for event in completed] == expected_languages
228
+ assert [event.completed for event in started] == list(range(len(expected_languages)))
229
+ assert [event.completed for event in completed] == list(range(1, len(expected_languages) + 1))
230
+ assert all(event.total == len(expected_languages) for event in started + completed)
231
+ assert all(event.pass_index == 2 and event.pass_total == 2 for event in started + completed)
@@ -54,6 +54,7 @@ from .model import (
54
54
  VoiceDirective,
55
55
  )
56
56
  from .planner import UtterancePlanner
57
+ from .progress import PlannerProgressEvent, ProgressCallback, ProgressKind, ProgressPhase
57
58
  from .versioning import (
58
59
  CURRENT_SCHEMA_VERSION,
59
60
  FORMAT,
@@ -75,6 +76,10 @@ __all__ = [
75
76
  "SSMDConfig",
76
77
  "parse_duration",
77
78
  "UtterancePlanner",
79
+ "PlannerProgressEvent",
80
+ "ProgressCallback",
81
+ "ProgressKind",
82
+ "ProgressPhase",
78
83
  "UtterancePlan",
79
84
  "PlanSource",
80
85
  "PlanTexts",
@@ -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.3.3'
22
- __version_tuple__ = version_tuple = (0, 3, 3)
21
+ __version__ = version = '0.3.4'
22
+ __version_tuple__ = version_tuple = (0, 3, 4)
23
23
 
24
- __commit_id__ = commit_id = 'g0e4e0751e'
24
+ __commit_id__ = commit_id = 'g938cb2c4a'
@@ -6,6 +6,7 @@ from typing import Literal
6
6
  from .config import PlannerConfig
7
7
  from .exceptions import ConfigurationError
8
8
  from .model import Diagnostic, UtterancePlan
9
+ from .progress import ProgressCallback
9
10
 
10
11
  InputFormat = Literal["ssmd", "plain"]
11
12
 
@@ -72,13 +73,14 @@ def compile_document(
72
73
  config: PlannerConfig,
73
74
  fallback_language: str | None = None,
74
75
  trace: bool = False,
76
+ on_progress: ProgressCallback | None = None,
75
77
  ) -> CompileResult:
76
78
  """Compile one SSMD document or explicitly selected plain-text document.
77
79
 
78
80
  The document's declared SSMD language is authoritative. ``fallback_language``
79
81
  replaces the configured fallback for this request; it never forces a language
80
82
  over document semantics. ``PlannerConfig.language`` remains the compatibility
81
- fallback when ``fallback_language`` is omitted.
83
+ fallback when ``fallback_language`` is omitted. The optional progress callback is synchronous and operational only; exceptions raised by it propagate to the caller.
82
84
  """
83
85
  if input_format not in {"ssmd", "plain"}:
84
86
  raise ConfigurationError("input_format must be 'ssmd' or 'plain'")
@@ -94,7 +96,7 @@ def compile_document(
94
96
 
95
97
  planner = UtterancePlanner(effective_config)
96
98
  try:
97
- return planner.compile(text, config=effective_config, trace=trace)
99
+ return planner.compile(text, config=effective_config, trace=trace, on_progress=on_progress)
98
100
  finally:
99
101
  planner.close()
100
102