utterplan 0.4.0__tar.gz → 0.4.1__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 (179) hide show
  1. {utterplan-0.4.0 → utterplan-0.4.1}/.github/workflows/tests.yml +2 -0
  2. {utterplan-0.4.0 → utterplan-0.4.1}/PKG-INFO +6 -1
  3. {utterplan-0.4.0 → utterplan-0.4.1}/README.md +5 -0
  4. {utterplan-0.4.0 → utterplan-0.4.1}/docs/changelog.md +20 -0
  5. {utterplan-0.4.0 → utterplan-0.4.1}/docs/cli.md +19 -0
  6. {utterplan-0.4.0 → utterplan-0.4.1}/docs/python-api.md +41 -0
  7. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_cli.py +59 -1
  8. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_renderability_repair.py +107 -0
  9. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_ssmd_09_parser.py +81 -0
  10. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/__init__.py +6 -0
  11. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/_version.py +3 -3
  12. utterplan-0.4.1/utterplan/attempt_serialization.py +245 -0
  13. utterplan-0.4.1/utterplan/attempts.py +103 -0
  14. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/cli.py +105 -1
  15. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/compiler.py +32 -0
  16. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/model.py +11 -6
  17. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/parsers.py +179 -60
  18. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/planner.py +102 -45
  19. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/toml_codec.py +22 -3
  20. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan.egg-info/PKG-INFO +6 -1
  21. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan.egg-info/SOURCES.txt +2 -0
  22. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan.egg-info/scm_file_list.json +14 -0
  23. utterplan-0.4.1/utterplan.egg-info/scm_version.json +8 -0
  24. utterplan-0.4.0/utterplan.egg-info/scm_version.json +0 -8
  25. {utterplan-0.4.0 → utterplan-0.4.1}/.codecrate.toml +0 -0
  26. {utterplan-0.4.0 → utterplan-0.4.1}/.github/workflows/codecov.yml +0 -0
  27. {utterplan-0.4.0 → utterplan-0.4.1}/.github/workflows/pre-commit.yml +0 -0
  28. {utterplan-0.4.0 → utterplan-0.4.1}/.github/workflows/python-publish.yml +0 -0
  29. {utterplan-0.4.0 → utterplan-0.4.1}/.gitignore +0 -0
  30. {utterplan-0.4.0 → utterplan-0.4.1}/.pre-commit-config.yaml +0 -0
  31. {utterplan-0.4.0 → utterplan-0.4.1}/AGENTS.md +0 -0
  32. {utterplan-0.4.0 → utterplan-0.4.1}/LICENSE +0 -0
  33. {utterplan-0.4.0 → utterplan-0.4.1}/MANIFEST.in +0 -0
  34. {utterplan-0.4.0 → utterplan-0.4.1}/NOTICE +0 -0
  35. {utterplan-0.4.0 → utterplan-0.4.1}/docs/Makefile +0 -0
  36. {utterplan-0.4.0 → utterplan-0.4.1}/docs/architecture.md +0 -0
  37. {utterplan-0.4.0 → utterplan-0.4.1}/docs/conf.py +0 -0
  38. {utterplan-0.4.0 → utterplan-0.4.1}/docs/consumer-guide.md +0 -0
  39. {utterplan-0.4.0 → utterplan-0.4.1}/docs/coordinate-spaces.md +0 -0
  40. {utterplan-0.4.0 → utterplan-0.4.1}/docs/debugging.md +0 -0
  41. {utterplan-0.4.0 → utterplan-0.4.1}/docs/format.md +0 -0
  42. {utterplan-0.4.0 → utterplan-0.4.1}/docs/getting-started.md +0 -0
  43. {utterplan-0.4.0 → utterplan-0.4.1}/docs/index.md +0 -0
  44. {utterplan-0.4.0 → utterplan-0.4.1}/docs/make.bat +0 -0
  45. {utterplan-0.4.0 → utterplan-0.4.1}/docs/phase-a-completion.md +0 -0
  46. {utterplan-0.4.0 → utterplan-0.4.1}/docs/pykokoro-integration.md +0 -0
  47. {utterplan-0.4.0 → utterplan-0.4.1}/docs/requirements.txt +0 -0
  48. {utterplan-0.4.0 → utterplan-0.4.1}/examples/basic.py +0 -0
  49. {utterplan-0.4.0 → utterplan-0.4.1}/examples/chapter.ssmd +0 -0
  50. {utterplan-0.4.0 → utterplan-0.4.1}/examples/compile_file.py +0 -0
  51. {utterplan-0.4.0 → utterplan-0.4.1}/examples/inspect_plan.py +0 -0
  52. {utterplan-0.4.0 → utterplan-0.4.1}/examples/multilingual.py +0 -0
  53. {utterplan-0.4.0 → utterplan-0.4.1}/examples/ssmd_example.py +0 -0
  54. {utterplan-0.4.0 → utterplan-0.4.1}/pyproject.toml +0 -0
  55. {utterplan-0.4.0 → utterplan-0.4.1}/setup.cfg +0 -0
  56. {utterplan-0.4.0 → utterplan-0.4.1}/spec/schemas/v1.schema.json +0 -0
  57. {utterplan-0.4.0 → utterplan-0.4.1}/spec/schemas/v2.schema.json +0 -0
  58. {utterplan-0.4.0 → utterplan-0.4.1}/spec/schemas/v3.schema.json +0 -0
  59. {utterplan-0.4.0 → utterplan-0.4.1}/spec/schemas/v4.schema.json +0 -0
  60. {utterplan-0.4.0 → utterplan-0.4.1}/spec/utterplan.schema.json +0 -0
  61. {utterplan-0.4.0 → utterplan-0.4.1}/tests/__init__.py +0 -0
  62. {utterplan-0.4.0 → utterplan-0.4.1}/tests/fixtures/canonical_consumer_contract.expected.toml +0 -0
  63. {utterplan-0.4.0 → utterplan-0.4.1}/tests/fixtures/canonical_consumer_contract.ssmd +0 -0
  64. {utterplan-0.4.0 → utterplan-0.4.1}/tests/fixtures/ssmd_09_comprehensive.ssmd +0 -0
  65. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/basic_en.utterplan.toml +0 -0
  66. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/directives.utterplan.toml +0 -0
  67. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/markers.utterplan.toml +0 -0
  68. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/multilingual.utterplan.toml +0 -0
  69. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/parenthetical.utterplan.toml +0 -0
  70. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/spokenform_offsets.utterplan.toml +0 -0
  71. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/ssmd_09_comprehensive.utterplan.toml +0 -0
  72. {utterplan-0.4.0 → utterplan-0.4.1}/tests/golden/ssmd_breaks.utterplan.toml +0 -0
  73. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/__init__.py +0 -0
  74. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/cases.py +0 -0
  75. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/basic_en.utterplan.json +0 -0
  76. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/directives.utterplan.json +0 -0
  77. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/markers.utterplan.json +0 -0
  78. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/multilingual.utterplan.json +0 -0
  79. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/parenthetical.utterplan.json +0 -0
  80. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/spokenform_offsets.utterplan.json +0 -0
  81. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/ssmd_09_comprehensive.utterplan.json +0 -0
  82. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/fixtures/v4/ssmd_breaks.utterplan.json +0 -0
  83. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/goldens/multilingual_preparation.json +0 -0
  84. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/goldens/spokenform_numbers.json +0 -0
  85. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/goldens/ssmd_break.json +0 -0
  86. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/goldens/ssmd_language_detection.json +0 -0
  87. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/goldens/ssmd_marker.json +0 -0
  88. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/goldens/ssmd_pronunciation.json +0 -0
  89. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/test_legacy_json_import.py +0 -0
  90. {utterplan-0.4.0 → utterplan-0.4.1}/tests/migration/test_semantic_migration_regressions.py +0 -0
  91. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/README.md +0 -0
  92. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v1/basic.json +0 -0
  93. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v1/directives.json +0 -0
  94. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v1/markers.json +0 -0
  95. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v1/multilingual.json +0 -0
  96. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v1/parenthetical.json +0 -0
  97. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v1/spokenform_offsets.json +0 -0
  98. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v1/ssmd_breaks.json +0 -0
  99. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v2/directives.json +0 -0
  100. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v3/multilingual.json +0 -0
  101. {utterplan-0.4.0 → utterplan-0.4.1}/tests/schema_history/v3/ssmd_09_comprehensive.json +0 -0
  102. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_atomic_io.py +0 -0
  103. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_batch.py +0 -0
  104. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_boundaries.py +0 -0
  105. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_cli_batch.py +0 -0
  106. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_compiler.py +0 -0
  107. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_consumer_contract.py +0 -0
  108. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_consumer_readiness.py +0 -0
  109. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_coordinates.py +0 -0
  110. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_directives.py +0 -0
  111. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_explain.py +0 -0
  112. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_fake_renderer.py +0 -0
  113. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_format.py +0 -0
  114. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_golden.py +0 -0
  115. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_identity_units.py +0 -0
  116. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_import_boundaries.py +0 -0
  117. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_language.py +0 -0
  118. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_linguistics.py +0 -0
  119. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_migration_framework.py +0 -0
  120. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_migration_v3_to_v4.py +0 -0
  121. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_pauses.py +0 -0
  122. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_planner.py +0 -0
  123. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_preparation.py +0 -0
  124. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_progress.py +0 -0
  125. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_public_identity.py +0 -0
  126. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_renderability.py +0 -0
  127. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_schema_history.py +0 -0
  128. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_schema_registry.py +0 -0
  129. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_schema_strict.py +0 -0
  130. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_segment_topology.py +0 -0
  131. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_semantic_boundaries.py +0 -0
  132. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_toml_format.py +0 -0
  133. {utterplan-0.4.0 → utterplan-0.4.1}/tests/test_version.py +0 -0
  134. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/__main__.py +0 -0
  135. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/atomic_io.py +0 -0
  136. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/batch.py +0 -0
  137. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/config.py +0 -0
  138. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/diagnostics.py +0 -0
  139. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/directives.py +0 -0
  140. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/exceptions.py +0 -0
  141. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/explain.py +0 -0
  142. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/format.py +0 -0
  143. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/hashing.py +0 -0
  144. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/language.py +0 -0
  145. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/linguistics.py +0 -0
  146. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/migration.py +0 -0
  147. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/migrations/__init__.py +0 -0
  148. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/migrations/registry.py +0 -0
  149. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/migrations/v1_to_v2.py +0 -0
  150. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/migrations/v2_to_v3.py +0 -0
  151. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/migrations/v3_to_v4.py +0 -0
  152. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/pauses.py +0 -0
  153. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/preparation.py +0 -0
  154. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/progress.py +0 -0
  155. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/py.typed +0 -0
  156. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/renderability.py +0 -0
  157. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/schema_registry.py +0 -0
  158. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/schemas/v1.schema.json +0 -0
  159. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/schemas/v2.schema.json +0 -0
  160. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/schemas/v3.schema.json +0 -0
  161. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/schemas/v4.schema.json +0 -0
  162. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/serialization.py +0 -0
  163. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/spacy_models.py +0 -0
  164. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/spans.py +0 -0
  165. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/__init__.py +0 -0
  166. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/document/__init__.py +0 -0
  167. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/document/plain.py +0 -0
  168. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/document/ssmd.py +0 -0
  169. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/preparation/__init__.py +0 -0
  170. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/preparation/identity.py +0 -0
  171. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/preparation/spokenform.py +0 -0
  172. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/stages/protocols.py +0 -0
  173. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/units.py +0 -0
  174. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/utterplan.schema.json +0 -0
  175. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan/versioning.py +0 -0
  176. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan.egg-info/dependency_links.txt +0 -0
  177. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan.egg-info/entry_points.txt +0 -0
  178. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan.egg-info/requires.txt +0 -0
  179. {utterplan-0.4.0 → utterplan-0.4.1}/utterplan.egg-info/top_level.txt +0 -0
@@ -53,6 +53,8 @@ jobs:
53
53
  "typing_extensions==4.0.0" \
54
54
  "tomlkit==0.13.0"
55
55
  python -m pip install --no-deps -e .
56
+ - name: Verify SSMD 0.9.3 front-matter scene-break handling
57
+ run: python -m pytest -q tests/test_ssmd_09_parser.py -k front_matter_scene_break
56
58
  - name: Run lower-bound tests
57
59
  run: python -m pytest -q
58
60
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utterplan
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -293,9 +293,14 @@ utterplan validate chapter.utterplan.toml
293
293
  utterplan inspect chapter.utterplan.toml --segment 0
294
294
  utterplan inspect chapter.utterplan.toml --semantic-boundaries
295
295
  utterplan explain chapter.utterplan.toml
296
+ utterplan inspect-attempt chapter.attempt.toml --issues
296
297
  ```
297
298
 
298
299
  `explain` presents the compiled plan as a human-readable speech plan, while `inspect` exposes lower-level diagnostic fields.
300
+
301
+ `inspect-attempt` reads the separate `utterplan.planning-attempt.v1` TOML artifact,
302
+ not a canonical plan. Use it to review renderability issues and safe repair
303
+ assessments on blocked planning outcomes; such candidates remain inspect-only drafts.
299
304
  Canonical plan output is TOML (`.utterplan.toml`) and is written to stdout when no output file is supplied. Status messages use stderr, and existing output files require `--force`. Safe punctuation repair is the default; use `--renderability strict` to reject every repair opportunity.
300
305
 
301
306
  ## SSMD source contract
@@ -50,9 +50,14 @@ utterplan validate chapter.utterplan.toml
50
50
  utterplan inspect chapter.utterplan.toml --segment 0
51
51
  utterplan inspect chapter.utterplan.toml --semantic-boundaries
52
52
  utterplan explain chapter.utterplan.toml
53
+ utterplan inspect-attempt chapter.attempt.toml --issues
53
54
  ```
54
55
 
55
56
  `explain` presents the compiled plan as a human-readable speech plan, while `inspect` exposes lower-level diagnostic fields.
57
+
58
+ `inspect-attempt` reads the separate `utterplan.planning-attempt.v1` TOML artifact,
59
+ not a canonical plan. Use it to review renderability issues and safe repair
60
+ assessments on blocked planning outcomes; such candidates remain inspect-only drafts.
56
61
  Canonical plan output is TOML (`.utterplan.toml`) and is written to stdout when no output file is supplied. Status messages use stderr, and existing output files require `--force`. Safe punctuation repair is the default; use `--renderability strict` to reject every repair opportunity.
57
62
 
58
63
  ## SSMD source contract
@@ -4,6 +4,26 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.1] - 2026-10-07
8
+
9
+ ### Added
10
+
11
+ - Added persistable planning attempts with inspectable renderability outcomes and safe repair assessments
12
+
13
+ ### Changed
14
+
15
+ - Changed planning APIs to expose blocked candidates without weakening canonical plan validation
16
+
17
+ ### Fixed
18
+
19
+ - Fixed legacy SSMD scene breaks to preserve front-matter boundaries and clean-to-source offsets
20
+
21
+ ### Documentation
22
+
23
+ - Documented planning-attempt artifacts, inspection workflows, and renderability guarantees
24
+
25
+ <!-- generated by releaseledger -->
26
+
7
27
  ## [0.4.0] - 2026-10-06
8
28
 
9
29
  ### Added
@@ -100,6 +100,24 @@ utterplan explain chapter.utterplan.toml --details
100
100
 
101
101
  The default output shows prepared wording, render units, ordered segments, languages, resolved pauses, headings, SSMD version/title/document language, effective typed directives, metadata, and warning/error codes with source locations. Add `--details` for IDs, offsets, provenance, hashes, plan identity, and token analysis beneath each segment. Use `inspect --tokens` for a compact token/provenance view.
102
102
 
103
+ ## Inspect a persisted planning attempt
104
+
105
+ `inspect` reads canonical plans. Persistable planning outcomes use the separate
106
+ `utterplan.planning-attempt.v1` artifact and are opened with `inspect-attempt`:
107
+
108
+ ```bash
109
+ utterplan inspect-attempt chapter.attempt.toml
110
+ utterplan inspect-attempt chapter.attempt.toml --issues
111
+ utterplan inspect-attempt chapter.attempt.toml --segment seg-000001 --unit unit-000001
112
+ utterplan inspect-attempt chapter.attempt.toml --json
113
+ ```
114
+
115
+ The default view summarizes status, renderability, and candidate size. `--issues`
116
+ shows source context and conservative repair assessments; segment and unit selectors
117
+ accept an ID or zero-based index. `--json` emits the complete artifact as JSON for
118
+ inspection, but the persisted attempt itself remains TOML. A blocked candidate is an
119
+ inspect-only draft, not a canonical plan, and is rejected by `UtterancePlan.load()`.
120
+
103
121
  ## Planning controls
104
122
 
105
123
  - `--unit paragraph|sentence` chooses render-unit grouping.
@@ -126,6 +144,7 @@ utterplan inspect chapter.utterplan.toml --segment 0
126
144
  utterplan inspect chapter.utterplan.toml --unit 0 --boundaries --tokens
127
145
  utterplan inspect chapter.utterplan.toml --preparation
128
146
  utterplan inspect chapter.utterplan.toml --semantic-boundaries
147
+ utterplan inspect-attempt chapter.attempt.toml --issues
129
148
  ```
130
149
 
131
150
  `inspect --preparation` reports the preparation backend and version, structural and spoken text lengths, replacement count, and each replacement's structural and spoken ranges and text. This is the supported human-facing preparation diagnostic; raw coordinate lookup tables are intentionally absent from the persisted TOML plan.
@@ -41,6 +41,47 @@ 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
+ ## Persistable planning attempts
45
+
46
+ `compile_attempt` and `UtterancePlanner.compile_attempt` expose a finalized planning
47
+ outcome without raising solely because a segment is not renderable. They still raise
48
+ for input, configuration, and planning failures. The existing `compile_document`,
49
+ `UtterancePlanner.compile`, and `plan` APIs remain strict and continue raising
50
+ `PlanRenderabilityError` when renderability blocks completion.
51
+
52
+ Attempts are separate from canonical plans. A blocked attempt retains an inspect-only
53
+ candidate draft; it is not a valid `UtterancePlan` and cannot be loaded by
54
+ `UtterancePlan.load()`. Persist it with `PlanningAttempt.save()` and restore it with
55
+ `PlanningAttempt.load()`; the attempt TOML schema is
56
+ `utterplan.planning-attempt.v1`, independent of canonical plan schema v4.
57
+
58
+ ```python
59
+ from utterplan import PlannerConfig, compile_attempt
60
+
61
+ attempt = compile_attempt(
62
+ "Hello.\n\n.\n\nWorld.",
63
+ input_format="plain",
64
+ config=PlannerConfig(
65
+ language="en-us",
66
+ renderability_mode="strict",
67
+ ),
68
+ )
69
+ assert attempt.status == "blocked"
70
+ attempt.save("chapter.attempt.toml")
71
+
72
+ for issue in attempt.renderability.issues:
73
+ assessment = issue.repair_assessment
74
+ if assessment is not None:
75
+ print(issue.segment_id, assessment.safe, assessment.action)
76
+ print(assessment.blockers)
77
+ ```
78
+
79
+ Repair assessments report the existing conservative options; they do not rewrite
80
+ text speculatively. In repair mode, only the planner's established safe repairs are
81
+ applied. `utterplan inspect-attempt chapter.attempt.toml --issues` shows issues,
82
+ source locations, and repair assessments; `--segment` and `--unit` select candidate
83
+ topology, and `--json` emits the complete attempt as JSON for inspection.
84
+
44
85
  ## TOML persistence
45
86
 
46
87
  Schema v4 remains the semantic contract, while `.utterplan.toml` is the canonical persisted format. `to_toml()` and `from_toml()` round-trip the complete plan; `save()` writes atomically, and `load()` accepts TOML only. Normal loading does not auto-detect or fall back to JSON.
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import pytest
8
8
 
9
- from utterplan import PlannerConfig, UtterancePlan, UtterancePlanner
9
+ from utterplan import PlannerConfig, UtterancePlan, UtterancePlanner, compile_attempt
10
10
  from utterplan.cli import build_parser, main
11
11
 
12
12
 
@@ -466,3 +466,61 @@ def test_validate_source_documents_and_preserve_saved_plan_validation(
466
466
  assert "plain input requires --language" in capsys.readouterr().err
467
467
  assert main(["validate", str(plain), "--input-format", "plain", "--language", "en-us"]) == 0
468
468
  assert "valid" in capsys.readouterr().out
469
+
470
+
471
+ def _save_blocked_attempt(tmp_path: Path):
472
+ config = PlannerConfig(
473
+ language="en-us",
474
+ document_format="plain",
475
+ text_preparation="identity",
476
+ renderability_mode="strict",
477
+ )
478
+ attempt = compile_attempt("Hello.\n\n.\n\nWorld.", input_format="plain", config=config)
479
+ path = tmp_path / "blocked.attempt.toml"
480
+ attempt.save(path)
481
+ return attempt, path
482
+
483
+
484
+ def test_inspect_attempt_reports_issues_segment_and_unit(
485
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
486
+ ) -> None:
487
+ attempt, path = _save_blocked_attempt(tmp_path)
488
+ issue_segment = attempt.renderability.issues[0].segment_id
489
+ unit = attempt.candidate.units[0]
490
+
491
+ assert (
492
+ main(
493
+ [
494
+ "inspect-attempt",
495
+ str(path),
496
+ "--issues",
497
+ "--segment",
498
+ issue_segment,
499
+ "--unit",
500
+ unit.id,
501
+ ]
502
+ )
503
+ == 0
504
+ )
505
+ output = capsys.readouterr().out
506
+ assert "status: blocked" in output
507
+ assert "renderability: blocked (strict)" in output
508
+ assert "renderability.punctuation_only" in output
509
+ assert "safe: true" in output
510
+ assert f"Segment {issue_segment}" in output
511
+ assert f"Unit {unit.id}" in output
512
+
513
+
514
+ def test_inspect_attempt_json_exposes_separate_artifact_schema(
515
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
516
+ ) -> None:
517
+ attempt, path = _save_blocked_attempt(tmp_path)
518
+
519
+ assert main(["inspect-attempt", str(path), "--json"]) == 0
520
+ output = capsys.readouterr().out
521
+ payload = json.loads(output)
522
+ assert payload["schema"] == "utterplan.planning-attempt.v1"
523
+ assert payload["attempt"]["status"] == "blocked"
524
+ assert payload["candidate"]["segments"]
525
+ assert payload["renderability"]["issues"]
526
+ assert payload["attempt"]["attempt_id"] == attempt.attempt_id
@@ -8,14 +8,17 @@ from utterplan import (
8
8
  AnnotationSpan,
9
9
  BoundaryEvent,
10
10
  EmphasisDirective,
11
+ PlanFormatError,
11
12
  PlannerConfig,
12
13
  PlanRenderabilityError,
13
14
  PlanSegment,
14
15
  SegmentDirectives,
15
16
  SemanticBoundary,
17
+ UtterancePlan,
16
18
  UtterancePlanner,
17
19
  assess_renderability_repair,
18
20
  classify_segment,
21
+ compile_attempt,
19
22
  )
20
23
  from utterplan.config import PauseConfig
21
24
  from utterplan.planner import _repair_renderer_segments
@@ -327,3 +330,107 @@ def test_symbol_only_content_fails_without_a_guessed_repair() -> None:
327
330
  assert issue.repair_assessment.safe is False
328
331
  assert "will not guess" in str(error.value)
329
332
  assert error.value.mode == mode
333
+
334
+
335
+ def test_strict_attempt_retains_blocked_candidate_and_compile_still_raises() -> None:
336
+ source = "Hello.\n\n.\n\nWorld."
337
+ config = PlannerConfig(
338
+ language="en-US",
339
+ text_preparation="identity",
340
+ renderability_mode="strict",
341
+ )
342
+ planner = UtterancePlanner(config)
343
+
344
+ attempt = planner.compile_attempt(source)
345
+ functional = compile_attempt(source, input_format="plain", config=config)
346
+ assert functional.status == "blocked"
347
+
348
+ assert attempt.status == "blocked"
349
+ assert not attempt.ok
350
+ assert attempt.renderability.issues
351
+ issue = attempt.renderability.issues[0]
352
+ segment = next(item for item in attempt.candidate.segments if item.id == issue.segment_id)
353
+ assert segment.text == issue.text == "."
354
+ assert issue.source_start is not None
355
+ assert issue.source_end is not None
356
+ assert source[issue.source_start : issue.source_end] == issue.text
357
+ assert attempt.candidate.plan_id == ""
358
+ with pytest.raises(ValueError, match="blocked planning-attempt draft"):
359
+ attempt.candidate.to_plan()
360
+
361
+ with pytest.raises(PlanRenderabilityError) as error:
362
+ planner.compile(source)
363
+ assert error.value.mode == "strict"
364
+ assert error.value.issues == attempt.renderability.issues
365
+
366
+
367
+ def test_safe_repair_attempt_returns_a_canonical_plan() -> None:
368
+ config = PlannerConfig(language="en-US", text_preparation="identity")
369
+ attempt = UtterancePlanner(config).compile_attempt("Hello.\n\n.\n\nWorld.")
370
+
371
+ assert attempt.status == "repaired"
372
+ assert attempt.ok
373
+ assert len(attempt.repairs) == 1
374
+ assert attempt.repairs[0].repair_assessment is not None
375
+ assert attempt.repairs[0].repair_assessment.safe
376
+ plan = attempt.candidate.to_plan()
377
+ plan.validate()
378
+ assert [segment.text for segment in plan.segments] == ["Hello.", "World."]
379
+
380
+
381
+ def test_unsafe_repair_attempt_is_blocked_and_draft_structure_is_checked() -> None:
382
+ from utterplan.exceptions import PlanValidationError
383
+ from utterplan.model import validate_plan_structure
384
+
385
+ config = PlannerConfig(
386
+ language="en-US",
387
+ text_preparation="identity",
388
+ renderability_mode="repair",
389
+ )
390
+ attempt = UtterancePlanner(config).compile_attempt("€")
391
+
392
+ assert attempt.status == "blocked"
393
+ assert attempt.renderability.issues[0].repair_assessment is not None
394
+ assert not attempt.renderability.issues[0].repair_assessment.safe
395
+ with pytest.raises(ValueError, match="blocked planning-attempt draft"):
396
+ attempt.candidate.to_plan()
397
+
398
+ malformed = replace(
399
+ attempt.candidate._plan,
400
+ segments=(replace(attempt.candidate.segments[0], spoken_end=100),),
401
+ )
402
+ with pytest.raises(PlanValidationError, match="segment range"):
403
+ validate_plan_structure(malformed)
404
+
405
+
406
+ @pytest.mark.parametrize(
407
+ ("source", "mode", "expected_status"),
408
+ [
409
+ ("Ready.", "strict", "renderable"),
410
+ ("Hello.\n\n.\n\nWorld.", "repair", "repaired"),
411
+ ("€", "strict", "blocked"),
412
+ ],
413
+ )
414
+ def test_planning_attempt_toml_roundtrips_all_outcomes_and_stays_noncanonical(
415
+ source: str, mode: str, expected_status: str
416
+ ) -> None:
417
+ config = PlannerConfig(
418
+ language="en-US",
419
+ text_preparation="identity",
420
+ renderability_mode=mode,
421
+ )
422
+ attempt = UtterancePlanner(config).compile_attempt(source)
423
+ repeated = UtterancePlanner(config).compile_attempt(source)
424
+ serialized = attempt.to_toml()
425
+ restored = type(attempt).from_toml(serialized)
426
+
427
+ assert attempt.status == expected_status
428
+ assert repeated.attempt_id == attempt.attempt_id
429
+ assert restored == attempt
430
+ assert restored.to_toml() == serialized
431
+ assert restored.to_dict() == attempt.to_dict()
432
+ assert 'schema = "utterplan.planning-attempt.v1"' in serialized
433
+ with pytest.raises(PlanFormatError):
434
+ UtterancePlan.from_toml(serialized)
435
+ with pytest.raises(PlanFormatError):
436
+ UtterancePlan.from_dict(attempt.to_dict())
@@ -39,6 +39,87 @@ def test_legacy_scene_break_is_structural_not_renderer_text() -> None:
39
39
  assert all(segment.text != "---" for segment in plan.segments)
40
40
 
41
41
 
42
+ @pytest.mark.parametrize(
43
+ ("body", "scene_break_count"),
44
+ [
45
+ ("Before.\n\n---\n\nAfter.", 1),
46
+ ("Before.\n\n---\n\nMiddle.\n\n---\n\nAfter.", 2),
47
+ ("Before.\n\n...p\n\nAfter.", 1),
48
+ ("---\n\nAfter.\n\n---", 2),
49
+ ("Before --- after.", 0),
50
+ ("Before.\n\n\\---\n\nAfter.", 0),
51
+ ],
52
+ )
53
+ def test_front_matter_scene_breaks_are_boundaries_not_renderer_text(
54
+ body: str, scene_break_count: int
55
+ ) -> None:
56
+ source = f'---\nssmd_version: "0.9"\ntitle: Demo\n---\n{body}'
57
+ plan = _planner().plan(source)
58
+
59
+ scene_breaks = [
60
+ event
61
+ for event in plan.boundaries
62
+ if event.kind == "explicit"
63
+ and (
64
+ event.attrs.get("semantic") == "scene_break"
65
+ or event.attrs.get("legacy_horizontal_rule")
66
+ or (event.origin == "ssmd" and event.strength == "x-strong")
67
+ )
68
+ ]
69
+ assert len(scene_breaks) == scene_break_count
70
+ if scene_break_count:
71
+ assert "---" not in plan.texts.structural or body.startswith("---")
72
+ assert all(segment.text.strip() != "---" for segment in plan.segments)
73
+ assert plan.document_metadata["planning"]["renderability"]["guaranteed"] is True
74
+ elif "Before --- after." in body:
75
+ assert "Before --- after." in plan.texts.structural
76
+ assert any("---" in segment.text for segment in plan.segments)
77
+ elif "\\---" in body:
78
+ assert "---" in plan.texts.structural
79
+
80
+
81
+ def test_legacy_parser_front_matter_scene_break_uses_clean_to_source_mapping(
82
+ monkeypatch: pytest.MonkeyPatch,
83
+ ) -> None:
84
+ import ssmd
85
+
86
+ from utterplan.parsers import SSMDDocumentParser, map_structural_span_to_source
87
+
88
+ header = '---\nssmd_version: "0.9"\ntitle: Demo\n---\n'
89
+ body = "Before.\n\n---\n\nAfter."
90
+ source = header + body
91
+ legacy_result = SimpleNamespace(
92
+ clean_text=body,
93
+ annotations=[],
94
+ events=[],
95
+ header={"ssmd_version": "0.9", "title": "Demo"},
96
+ warnings=[],
97
+ diagnostics=[],
98
+ text_spans=[
99
+ SimpleNamespace(
100
+ char_start=0,
101
+ char_end=len(body),
102
+ source_start=len(header),
103
+ source_end=len(source),
104
+ )
105
+ ],
106
+ )
107
+ monkeypatch.setattr(ssmd, "parse_structure", lambda *args, **kwargs: legacy_result)
108
+
109
+ parsed = SSMDDocumentParser().parse(source, _planner().config)
110
+ assert parsed.structural_text == "Before.\n\nAfter."
111
+ source_after = source.index("After.")
112
+ assert map_structural_span_to_source(
113
+ parsed, len("Before.\n\n"), len(parsed.structural_text)
114
+ ) == (source_after, source_after + len("After."))
115
+ scene = next(event for event in parsed.boundaries if event.attrs.get("legacy_horizontal_rule"))
116
+ assert scene.attrs["source_start"] == source.index("---", len(header))
117
+ assert scene.attrs["source_end"] == source_after
118
+
119
+ plan = _planner().plan(source)
120
+ assert [segment.text for segment in plan.segments] == ["Before.", "After."]
121
+
122
+
42
123
  def _planner() -> UtterancePlanner:
43
124
  return UtterancePlanner(
44
125
  PlannerConfig(
@@ -1,4 +1,5 @@
1
1
  from ._version import __version__
2
+ from .attempts import PlanDraft, PlanningAttempt, PlanningAttemptStatus
2
3
  from .batch import BatchProgressCallback, CompileOutcome, CompileRequest, compile_to_files
3
4
  from .compiler import (
4
5
  CompileResult,
@@ -6,6 +7,7 @@ from .compiler import (
6
7
  PreparationChange,
7
8
  PreparationTrace,
8
9
  PreparationTraceUnit,
10
+ compile_attempt,
9
11
  compile_document,
10
12
  )
11
13
  from .config import (
@@ -89,6 +91,10 @@ __all__ = [
89
91
  "PreparationTrace",
90
92
  "PreparationTraceUnit",
91
93
  "compile_document",
94
+ "compile_attempt",
95
+ "PlanningAttempt",
96
+ "PlanningAttemptStatus",
97
+ "PlanDraft",
92
98
  "compile_to_files",
93
99
  "PlannerConfig",
94
100
  "PauseConfig",
@@ -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.4.0'
22
- __version_tuple__ = version_tuple = (0, 4, 0)
21
+ __version__ = version = '0.4.1'
22
+ __version_tuple__ = version_tuple = (0, 4, 1)
23
23
 
24
- __commit_id__ = commit_id = 'gc73c43017'
24
+ __commit_id__ = commit_id = 'g293fe7cd5'