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