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