renderplan 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/events/events.jsonl +8 -0
  2. renderplan-0.1.4/.ledger/releaseledger/data/ledgers/main/releases/v0.1.3/audit/commit-audit.yaml +39 -0
  3. renderplan-0.1.4/.ledger/releaseledger/data/ledgers/main/releases/v0.1.3/entries/entry-0001.md +31 -0
  4. renderplan-0.1.4/.ledger/releaseledger/data/ledgers/main/releases/v0.1.3/release.md +27 -0
  5. renderplan-0.1.4/.ledger/releaseledger/data/ledgers/main/releases/v0.1.4/entries/entry-0001.md +29 -0
  6. renderplan-0.1.4/.ledger/releaseledger/data/ledgers/main/releases/v0.1.4/release.md +27 -0
  7. renderplan-0.1.4/.ledger/releaseledger/work/v0.1.3/audit-decisions.yaml +14 -0
  8. renderplan-0.1.4/.ledger/releaseledger/work/v0.1.3/audit.yaml +31 -0
  9. renderplan-0.1.4/.ledger/releaseledger/work/v0.1.3/entries.yaml +26 -0
  10. renderplan-0.1.4/.ledger/releaseledger/work/v0.1.3/evidence/8c9c937.patch +89 -0
  11. renderplan-0.1.4/.ledger/releaseledger/work/v0.1.3/evidence/manifest.json +27 -0
  12. renderplan-0.1.4/.ledger/releaseledger/work/v0.1.3/range.json +13 -0
  13. {renderplan-0.1.2/renderplan.egg-info → renderplan-0.1.4}/PKG-INFO +21 -1
  14. {renderplan-0.1.2 → renderplan-0.1.4}/README.md +20 -0
  15. {renderplan-0.1.2 → renderplan-0.1.4}/docs/changelog.md +16 -0
  16. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/__init__.py +16 -1
  17. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/_version.py +3 -3
  18. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/codec.py +7 -0
  19. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/contracts.py +17 -1
  20. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/model.py +23 -0
  21. {renderplan-0.1.2 → renderplan-0.1.4/renderplan.egg-info}/PKG-INFO +21 -1
  22. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan.egg-info/SOURCES.txt +12 -0
  23. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan.egg-info/scm_file_list.json +12 -0
  24. renderplan-0.1.4/renderplan.egg-info/scm_version.json +8 -0
  25. {renderplan-0.1.2 → renderplan-0.1.4}/tests/test_contracts.py +82 -0
  26. {renderplan-0.1.2 → renderplan-0.1.4}/tests/test_identity.py +18 -0
  27. renderplan-0.1.4/tests/test_profile_reidentification.py +40 -0
  28. renderplan-0.1.2/renderplan.egg-info/scm_version.json +0 -8
  29. {renderplan-0.1.2 → renderplan-0.1.4}/.codecrate.toml +0 -0
  30. {renderplan-0.1.2 → renderplan-0.1.4}/.github/workflows/codecov.yml +0 -0
  31. {renderplan-0.1.2 → renderplan-0.1.4}/.github/workflows/pre-commit.yml +0 -0
  32. {renderplan-0.1.2 → renderplan-0.1.4}/.github/workflows/python-publish.yml +0 -0
  33. {renderplan-0.1.2 → renderplan-0.1.4}/.github/workflows/tests.yml +0 -0
  34. {renderplan-0.1.2 → renderplan-0.1.4}/.gitignore +0 -0
  35. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/ledger.toml +0 -0
  36. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/.ledger-project.toml +0 -0
  37. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/config.toml +0 -0
  38. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/.ledger-project.toml +0 -0
  39. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.0/audit/commit-audit.yaml +0 -0
  40. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.0/entries/entry-0001.md +0 -0
  41. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.0/entries/entry-0002.md +0 -0
  42. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.0/entries/entry-0003.md +0 -0
  43. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.0/entries/entry-0004.md +0 -0
  44. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.0/release.md +0 -0
  45. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.1/audit/commit-audit.yaml +0 -0
  46. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.1/entries/entry-0001.md +0 -0
  47. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.1/entries/entry-0002.md +0 -0
  48. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.1/release.md +0 -0
  49. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.2/audit/commit-audit.yaml +0 -0
  50. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.2/entries/entry-0001.md +0 -0
  51. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/data/ledgers/main/releases/v0.1.2/release.md +0 -0
  52. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.1/audit-decisions.yaml +0 -0
  53. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.1/audit.yaml +0 -0
  54. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.1/entries.yaml +0 -0
  55. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.1/evidence/0e39bcd.patch +0 -0
  56. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.1/evidence/f27def9.patch +0 -0
  57. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.1/evidence/manifest.json +0 -0
  58. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.1/range.json +0 -0
  59. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.2/audit-decisions.yaml +0 -0
  60. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.2/audit.yaml +0 -0
  61. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.2/entries.yaml +0 -0
  62. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.2/evidence/f88173e.patch +0 -0
  63. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.2/evidence/manifest.json +0 -0
  64. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/work/v0.1.2/range.json +0 -0
  65. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/releaseledger/write.lock +0 -0
  66. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/taskledger/.ledger-project.toml +0 -0
  67. {renderplan-0.1.2 → renderplan-0.1.4}/.ledger/taskledger/config.toml +0 -0
  68. {renderplan-0.1.2 → renderplan-0.1.4}/.pre-commit-config.yaml +0 -0
  69. {renderplan-0.1.2 → renderplan-0.1.4}/LICENSE +0 -0
  70. {renderplan-0.1.2 → renderplan-0.1.4}/docs/Makefile +0 -0
  71. {renderplan-0.1.2 → renderplan-0.1.4}/docs/conf.py +0 -0
  72. {renderplan-0.1.2 → renderplan-0.1.4}/docs/make.bat +0 -0
  73. {renderplan-0.1.2 → renderplan-0.1.4}/docs/make.py +0 -0
  74. {renderplan-0.1.2 → renderplan-0.1.4}/docs/requirements.txt +0 -0
  75. {renderplan-0.1.2 → renderplan-0.1.4}/examples/af_heart.renderprofile.toml +0 -0
  76. {renderplan-0.1.2 → renderplan-0.1.4}/examples/chapter.preflight.toml +0 -0
  77. {renderplan-0.1.2 → renderplan-0.1.4}/examples/chapter.render-manifest.toml +0 -0
  78. {renderplan-0.1.2 → renderplan-0.1.4}/examples/chapter.renderplan.toml +0 -0
  79. {renderplan-0.1.2 → renderplan-0.1.4}/examples/chapter.replan.preflight.toml +0 -0
  80. {renderplan-0.1.2 → renderplan-0.1.4}/pyproject.toml +0 -0
  81. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/__main__.py +0 -0
  82. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/_toml.py +0 -0
  83. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/cli.py +0 -0
  84. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/collection.py +0 -0
  85. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/errors.py +0 -0
  86. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/identity.py +0 -0
  87. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/paths.py +0 -0
  88. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/py.typed +0 -0
  89. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/set.py +0 -0
  90. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/validation.py +0 -0
  91. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan/version.py +0 -0
  92. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan.egg-info/dependency_links.txt +0 -0
  93. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan.egg-info/entry_points.txt +0 -0
  94. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan.egg-info/requires.txt +0 -0
  95. {renderplan-0.1.2 → renderplan-0.1.4}/renderplan.egg-info/top_level.txt +0 -0
  96. {renderplan-0.1.2 → renderplan-0.1.4}/setup.cfg +0 -0
  97. {renderplan-0.1.2 → renderplan-0.1.4}/tests/test_collection.py +0 -0
  98. {renderplan-0.1.2 → renderplan-0.1.4}/tests/test_renderplan_set.py +0 -0
@@ -16,3 +16,11 @@
16
16
  {"data":{"entry_ids":["entry-0001"],"synced_audit_rows":1},"event":"entry.batch_added","event_id":"event-0016","record_revisions":{"commit_audit_sheet":3,"entry:v0.1.2/entry-0001":1,"release:v0.1.2":3},"release_version":"v0.1.2","schema_version":2}
17
17
  {"data":{"released_at":"2026-10-13"},"event":"release.finalized","event_id":"event-0017","record_revisions":{"release:v0.1.1":4},"release_version":"v0.1.1","schema_version":2}
18
18
  {"data":{"released_at":"2026-10-14"},"event":"release.finalized","event_id":"event-0018","record_revisions":{"release:v0.1.2":4},"release_version":"v0.1.2","schema_version":2}
19
+ {"data":{"status":"planned"},"event":"release.created","event_id":"event-0019","record_revisions":{"release:v0.1.3":1},"release_version":"v0.1.3","schema_version":2}
20
+ {"data":{"fields":["git_base_ref","git_base_sha","git_commit_count","git_head_ref","git_head_sha","git_range"]},"event":"release.updated","event_id":"event-0020","record_revisions":{"release:v0.1.3":2},"release_version":"v0.1.3","schema_version":2}
21
+ {"data":{"row_count":1},"event":"audit.created","event_id":"event-0021","record_revisions":{"commit_audit_sheet":1},"release_version":"v0.1.3","schema_version":2}
22
+ {"data":{"updated_rows":1},"event":"audit.updated","event_id":"event-0022","record_revisions":{"commit_audit_sheet":2},"release_version":"v0.1.3","schema_version":2}
23
+ {"data":{"entry_ids":["entry-0001"],"synced_audit_rows":1},"event":"entry.batch_added","event_id":"event-0023","record_revisions":{"commit_audit_sheet":3,"entry:v0.1.3/entry-0001":1,"release:v0.1.3":3},"release_version":"v0.1.3","schema_version":2}
24
+ {"data":{"released_at":"2026-10-25"},"event":"release.finalized","event_id":"event-0024","record_revisions":{"release:v0.1.3":4},"release_version":"v0.1.3","schema_version":2}
25
+ {"data":{"status":"planned"},"event":"release.created","event_id":"event-0025","record_revisions":{"release:v0.1.4":1},"release_version":"v0.1.4","schema_version":2}
26
+ {"data":{"kind":"added","status":"accepted"},"entry_id":"entry-0001","event":"entry.added","event_id":"event-0026","record_revisions":{"entry:v0.1.4/entry-0001":1,"release:v0.1.4":2},"release_version":"v0.1.4","schema_version":2}
@@ -0,0 +1,39 @@
1
+ schema_version: 1
2
+ object_type: commit_audit_sheet
3
+ versioning:
4
+ schema_version: 1
5
+ revision: 3
6
+ release_version: v0.1.3
7
+ git_base_ref: v0.1.2
8
+ git_base_sha: c2981a645889c974f2f7dc8b93ee8c3828655d9d
9
+ git_head_ref: HEAD
10
+ git_head_sha: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
11
+ git_range: c2981a645889c974f2f7dc8b93ee8c3828655d9d..8c9c93738b8e26a66b25b0cbefc04c070f1ec227
12
+ commit_count: 1
13
+ rows:
14
+ - sha: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
15
+ source_ref: git:8c9c93738b8e26a66b25b0cbefc04c070f1ec227
16
+ short_sha: 8c9c937
17
+ changed_paths:
18
+ - renderplan/__init__.py
19
+ - renderplan/codec.py
20
+ - tests/test_profile_reidentification.py
21
+ stats:
22
+ files_changed: 3
23
+ insertions: 49
24
+ deletions: 0
25
+ inspected: true
26
+ inspected_paths:
27
+ - renderplan/__init__.py
28
+ - renderplan/codec.py
29
+ - tests/test_profile_reidentification.py
30
+ observed_behavior:
31
+ Added a public loader that clears only a stored profile identity
32
+ before validation, allowing re-identification while preserving validation of the
33
+ remaining profile data; added coverage for stale identities and malformed TOML.
34
+ public_impact: public
35
+ decision: accepted
36
+ stale: false
37
+ evidence_subject: "feat: recreate profile_id"
38
+ target_entry_id: entry-0001
39
+ notes: ""
@@ -0,0 +1,31 @@
1
+ ---
2
+ schema_version: 2
3
+ object_type: release_entry
4
+ versioning:
5
+ schema_version: 1
6
+ revision: 1
7
+ entry_id: entry-0001
8
+ release_version: v0.1.3
9
+ kind: added
10
+ summary:
11
+ Added profile loading for re-identification while preserving validation of
12
+ other profile data
13
+ status: accepted
14
+ audience: null
15
+ scopes: []
16
+ source_refs:
17
+ - git:8c9c93738b8e26a66b25b0cbefc04c070f1ec227
18
+ paths:
19
+ - renderplan/__init__.py
20
+ - renderplan/codec.py
21
+ - tests/test_profile_reidentification.py
22
+ issues: []
23
+ prs: []
24
+ sources:
25
+ - git:8c9c93738b8e26a66b25b0cbefc04c070f1ec227
26
+ contributors:
27
+ - "@holgern"
28
+ breaking: false
29
+ internal: false
30
+ order: 1
31
+ ---
@@ -0,0 +1,27 @@
1
+ ---
2
+ schema_version: 2
3
+ object_type: release
4
+ versioning:
5
+ schema_version: 1
6
+ revision: 4
7
+ version: v0.1.3
8
+ status: released
9
+ history_state: curated
10
+ title: null
11
+ released_at: "2026-10-25"
12
+ previous_version: v0.1.2
13
+ cancel_reason: null
14
+ superseded_by: null
15
+ changelog_file: docs/changelog.md
16
+ boundary_ref: null
17
+ source_refs: []
18
+ source_count: null
19
+ entry_count: 1
20
+ artifact_count: 0
21
+ git_base_ref: v0.1.2
22
+ git_base_sha: c2981a645889c974f2f7dc8b93ee8c3828655d9d
23
+ git_head_ref: HEAD
24
+ git_head_sha: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
25
+ git_range: c2981a645889c974f2f7dc8b93ee8c3828655d9d..8c9c93738b8e26a66b25b0cbefc04c070f1ec227
26
+ git_commit_count: 1
27
+ ---
@@ -0,0 +1,29 @@
1
+ ---
2
+ schema_version: 2
3
+ object_type: release_entry
4
+ versioning:
5
+ schema_version: 1
6
+ revision: 1
7
+ entry_id: entry-0001
8
+ release_version: v0.1.4
9
+ kind: added
10
+ summary: Added portable character interpretation semantics to speech controls
11
+ status: accepted
12
+ audience: null
13
+ scopes: []
14
+ source_refs:
15
+ - tl:task-0004
16
+ paths:
17
+ - renderplan/model.py
18
+ - renderplan/contracts.py
19
+ - README.md
20
+ issues: []
21
+ prs: []
22
+ sources: []
23
+ contributors: []
24
+ breaking: false
25
+ internal: false
26
+ order: 1
27
+ ---
28
+
29
+ RenderPlan formalizes say_as parameter validation while preserving exact request text and leaving engine-specific realization to renderer preflight.
@@ -0,0 +1,27 @@
1
+ ---
2
+ schema_version: 2
3
+ object_type: release
4
+ versioning:
5
+ schema_version: 1
6
+ revision: 2
7
+ version: v0.1.4
8
+ status: planned
9
+ history_state: curated
10
+ title: null
11
+ released_at: null
12
+ previous_version: v0.1.3
13
+ cancel_reason: null
14
+ superseded_by: null
15
+ changelog_file: docs/changelog.md
16
+ boundary_ref: null
17
+ source_refs: []
18
+ source_count: null
19
+ entry_count: 1
20
+ artifact_count: 0
21
+ git_base_ref: null
22
+ git_base_sha: null
23
+ git_head_ref: null
24
+ git_head_sha: null
25
+ git_range: null
26
+ git_commit_count: null
27
+ ---
@@ -0,0 +1,14 @@
1
+ rows:
2
+ - sha: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
3
+ inspected: true
4
+ inspected_paths:
5
+ - renderplan/__init__.py
6
+ - renderplan/codec.py
7
+ - tests/test_profile_reidentification.py
8
+ observed_behavior: "Added a public loader that clears only a stored profile identity before validation, allowing re-identification while preserving validation of the remaining profile data; added coverage for stale identities and malformed TOML."
9
+ public_impact: public
10
+ decision: accepted
11
+ target_entry_key: null
12
+ target_entry_id: null
13
+ notes: ""
14
+ stale: false
@@ -0,0 +1,31 @@
1
+ schema_version: 1
2
+ object_type: commit_audit_sheet
3
+ versioning:
4
+ schema_version: 1
5
+ revision: 1
6
+ release_version: v0.1.3
7
+ git_base_ref: v0.1.2
8
+ git_base_sha: c2981a645889c974f2f7dc8b93ee8c3828655d9d
9
+ git_head_ref: HEAD
10
+ git_head_sha: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
11
+ git_range: c2981a645889c974f2f7dc8b93ee8c3828655d9d..8c9c93738b8e26a66b25b0cbefc04c070f1ec227
12
+ commit_count: 1
13
+ rows:
14
+ - sha: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
15
+ source_ref: git:8c9c93738b8e26a66b25b0cbefc04c070f1ec227
16
+ short_sha: 8c9c937
17
+ changed_paths:
18
+ - renderplan/__init__.py
19
+ - renderplan/codec.py
20
+ - tests/test_profile_reidentification.py
21
+ stats:
22
+ files_changed: 3
23
+ insertions: 49
24
+ deletions: 0
25
+ inspected: false
26
+ inspected_paths: []
27
+ observed_behavior: ""
28
+ public_impact: unknown
29
+ decision: needs_review
30
+ stale: false
31
+ evidence_subject: "feat: recreate profile_id"
@@ -0,0 +1,26 @@
1
+ schema: releaseledger.entry-batch.v1
2
+ operation: create
3
+ schema_version: 1
4
+ object_type: release_entry_batch
5
+ release_version: v0.1.3
6
+ git_base_ref: c2981a645889c974f2f7dc8b93ee8c3828655d9d
7
+ git_base_sha: c2981a645889c974f2f7dc8b93ee8c3828655d9d
8
+ git_head_ref: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
9
+ git_head_sha: 8c9c93738b8e26a66b25b0cbefc04c070f1ec227
10
+ include_merges: nontrivial
11
+ entries:
12
+ - kind: added
13
+ summary: "Added profile loading for re-identification while preserving validation of other profile data"
14
+ status: accepted
15
+ source_refs:
16
+ - git:8c9c93738b8e26a66b25b0cbefc04c070f1ec227
17
+ paths:
18
+ - renderplan/__init__.py
19
+ - renderplan/codec.py
20
+ - tests/test_profile_reidentification.py
21
+ sources:
22
+ - git:8c9c93738b8e26a66b25b0cbefc04c070f1ec227
23
+ prs: []
24
+ issues: []
25
+ contributors:
26
+ - "@holgern"
@@ -0,0 +1,89 @@
1
+ renderplan/__init__.py | 2 ++
2
+ renderplan/codec.py | 7 ++++++
3
+ tests/test_profile_reidentification.py | 40 ++++++++++++++++++++++++++++++++++
4
+ 3 files changed, 49 insertions(+)
5
+
6
+ diff --git a/renderplan/__init__.py b/renderplan/__init__.py
7
+ index bfc7f94..fb1ae5e 100644
8
+ --- a/renderplan/__init__.py
9
+ +++ b/renderplan/__init__.py
10
+ @@ -4,6 +4,7 @@ from .codec import (
11
+ load_plan,
12
+ load_preflight,
13
+ load_profile,
14
+ + load_profile_for_reidentification,
15
+ save_manifest,
16
+ save_plan,
17
+ save_preflight,
18
+ @@ -102,6 +103,7 @@ __all__ = [
19
+ "load_plan",
20
+ "load_preflight",
21
+ "load_profile",
22
+ + "load_profile_for_reidentification",
23
+ "load_renderplan_set",
24
+ "renderplan_set_from_dict",
25
+ "renderplan_set_to_dict",
26
+ diff --git a/renderplan/codec.py b/renderplan/codec.py
27
+ index 060982a..b445a1f 100644
28
+ --- a/renderplan/codec.py
29
+ +++ b/renderplan/codec.py
30
+ @@ -649,6 +649,13 @@ def load_profile(path: str | Path) -> RenderProfile:
31
+ return profile_from_dict(_read(path))
32
+
33
+
34
+ +def load_profile_for_reidentification(path: str | Path) -> RenderProfile:
35
+ + """Load and validate a profile while ignoring only its stored identity."""
36
+ + raw = dict(_read(path))
37
+ + raw["profile_id"] = None
38
+ + return profile_from_dict(raw)
39
+ +
40
+ +
41
+ def save_profile(profile: RenderProfile, path: str | Path) -> Path:
42
+ return _write(path, profile_to_dict(profile.with_identity()))
43
+
44
+ diff --git a/tests/test_profile_reidentification.py b/tests/test_profile_reidentification.py
45
+ new file mode 100644
46
+ index 0000000..eb45a35
47
+ --- /dev/null
48
+ +++ b/tests/test_profile_reidentification.py
49
+ @@ -0,0 +1,40 @@
50
+ +from __future__ import annotations
51
+ +
52
+ +from pathlib import Path
53
+ +
54
+ +import pytest
55
+ +
56
+ +from renderplan import (
57
+ + CodecError,
58
+ + ValidationError,
59
+ + load_profile,
60
+ + load_profile_for_reidentification,
61
+ +)
62
+ +
63
+ +
64
+ +def test_reidentification_loader_ignores_only_stored_profile_id(tmp_path: Path) -> None:
65
+ + source = Path(__file__).parents[1] / "examples/af_heart.renderprofile.toml"
66
+ + content = source.read_text(encoding="utf-8")
67
+ +
68
+ + # Replace the complete stored digest without relying on its current value.
69
+ + lines = content.splitlines()
70
+ + id_line = next(index for index, line in enumerate(lines) if line.startswith("profile_id ="))
71
+ + lines[id_line] = 'profile_id = "sha256:' + "0" * 64 + '"'
72
+ + path = tmp_path / "stale.renderprofile.toml"
73
+ + path.write_text("\n".join(lines) + "\n", encoding="utf-8")
74
+ +
75
+ + with pytest.raises(ValidationError):
76
+ + load_profile(path)
77
+ +
78
+ + profile = load_profile_for_reidentification(path)
79
+ + assert profile.profile_id is None
80
+ + repaired = profile.with_identity()
81
+ + assert repaired.profile_id == repaired.computed_id()
82
+ +
83
+ +
84
+ +def test_reidentification_loader_still_rejects_malformed_toml(tmp_path: Path) -> None:
85
+ + path = tmp_path / "invalid.renderprofile.toml"
86
+ + path.write_text("not = [valid", encoding="utf-8")
87
+ +
88
+ + with pytest.raises(CodecError):
89
+ + load_profile_for_reidentification(path)
@@ -0,0 +1,27 @@
1
+ {
2
+ "candidate_count": 1,
3
+ "candidates": [
4
+ {
5
+ "additions": 49,
6
+ "deletions": 0,
7
+ "patch_file": "8c9c937.patch",
8
+ "patch_sha256": "d7874770d8f33fb0aee2f49623e34d18ca931becbe00f18830d0c5c51dcb4bc8",
9
+ "paths": [
10
+ "renderplan/__init__.py",
11
+ "renderplan/codec.py",
12
+ "tests/test_profile_reidentification.py"
13
+ ],
14
+ "sha": "8c9c93738b8e26a66b25b0cbefc04c070f1ec227",
15
+ "short_sha": "8c9c937",
16
+ "source_ref": "git:8c9c93738b8e26a66b25b0cbefc04c070f1ec227"
17
+ }
18
+ ],
19
+ "git_base_ref": "c2981a645889c974f2f7dc8b93ee8c3828655d9d",
20
+ "git_base_sha": "c2981a645889c974f2f7dc8b93ee8c3828655d9d",
21
+ "git_head_ref": "8c9c93738b8e26a66b25b0cbefc04c070f1ec227",
22
+ "git_head_sha": "8c9c93738b8e26a66b25b0cbefc04c070f1ec227",
23
+ "include_merges": "nontrivial",
24
+ "object_type": "git_evidence_manifest",
25
+ "release_version": "v0.1.3",
26
+ "schema_version": 1
27
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "base_ref": "c2981a645889c974f2f7dc8b93ee8c3828655d9d",
3
+ "base_sha": "c2981a645889c974f2f7dc8b93ee8c3828655d9d",
4
+ "candidate_count": 1,
5
+ "commit_count": 1,
6
+ "head_ref": "8c9c93738b8e26a66b25b0cbefc04c070f1ec227",
7
+ "head_sha": "8c9c93738b8e26a66b25b0cbefc04c070f1ec227",
8
+ "include_merges": "nontrivial",
9
+ "kind": "git_range_summary",
10
+ "merge_commit_count": 0,
11
+ "merge_commits_skipped": 0,
12
+ "range": "c2981a645889c974f2f7dc8b93ee8c3828655d9d..8c9c93738b8e26a66b25b0cbefc04c070f1ec227"
13
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderplan
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Engine-neutral render profile, plan, preflight, manifest, and collection contracts for speech pipelines.
5
5
  Classifier: Development Status :: 3 - Alpha
6
6
  Classifier: Programming Language :: Python :: 3
@@ -90,6 +90,26 @@ The destination start offset of source span `N` inside the request is the sum of
90
90
 
91
91
  `SpeechControl` is request-local and renderer-neutral: it carries `start`, `end`, `name`, and TOML-safe `params`. Initial semantic names include `pronunciation`, `prosody`, `emphasis`, `say_as`, and `extension:<name>`. Controls participate in `segment_id`; renderers decide support, and unsupported controls must fail preflight rather than be ignored. Control ranges may overlap only when the source semantics permit it. Engine-specific voice selection is resolved into the opaque logical role/binding; audio directives become `AudioItem`s.
92
92
 
93
+ ### Standard `say_as` semantics
94
+
95
+ `SpeechControl(name="say_as")` requires a non-empty `interpret_as` string and accepts optional non-empty `format` and `detail` strings. The standard value `interpret_as = "characters"` means: speak the covered text as a sequence of separately pronounced characters, preserving the surrounding request as one continuous speech request when the engine can do so. It is portable intent; RenderPlan does not dictate an engine's realization or restrict future `interpret_as` values.
96
+
97
+ For example, the following control covers exactly `A R T`:
98
+
99
+ ```toml
100
+ [[flow.control]]
101
+ start = 0
102
+ end = 5
103
+ name = "say_as"
104
+
105
+ [flow.control.params]
106
+ interpret_as = "characters"
107
+ ```
108
+
109
+ The exact-range invariant is `speech.text[0:5] == "A R T"`. The control does not rewrite the canonical request text, prescribe spaces, hyphens, commas, SSML, phonemes, or pauses, and does not split a sentence or add a `PauseItem`. A renderer that does not support the requested semantics must reject the request during preflight rather than silently ignore the control.
110
+
111
+ Do not store `A- R- T` in RenderPlan merely because one Kokoro frontend prefers that spelling; an engine may choose such a frontend-specific realization internally.
112
+
93
113
  Bindings copied into plans must be self-contained enough to execute without reopening the RenderProfile. This contract does not define renderer-specific path keys or local-asset resolution.
94
114
  This schema has no generic asset-reference type yet. A VoiceRender profile producer must refuse bindings that depend on profile-local or otherwise uncopied asset state; do not introduce renderer-specific asset-path keys into this shared contract.
95
115
 
@@ -67,6 +67,26 @@ The destination start offset of source span `N` inside the request is the sum of
67
67
 
68
68
  `SpeechControl` is request-local and renderer-neutral: it carries `start`, `end`, `name`, and TOML-safe `params`. Initial semantic names include `pronunciation`, `prosody`, `emphasis`, `say_as`, and `extension:<name>`. Controls participate in `segment_id`; renderers decide support, and unsupported controls must fail preflight rather than be ignored. Control ranges may overlap only when the source semantics permit it. Engine-specific voice selection is resolved into the opaque logical role/binding; audio directives become `AudioItem`s.
69
69
 
70
+ ### Standard `say_as` semantics
71
+
72
+ `SpeechControl(name="say_as")` requires a non-empty `interpret_as` string and accepts optional non-empty `format` and `detail` strings. The standard value `interpret_as = "characters"` means: speak the covered text as a sequence of separately pronounced characters, preserving the surrounding request as one continuous speech request when the engine can do so. It is portable intent; RenderPlan does not dictate an engine's realization or restrict future `interpret_as` values.
73
+
74
+ For example, the following control covers exactly `A R T`:
75
+
76
+ ```toml
77
+ [[flow.control]]
78
+ start = 0
79
+ end = 5
80
+ name = "say_as"
81
+
82
+ [flow.control.params]
83
+ interpret_as = "characters"
84
+ ```
85
+
86
+ The exact-range invariant is `speech.text[0:5] == "A R T"`. The control does not rewrite the canonical request text, prescribe spaces, hyphens, commas, SSML, phonemes, or pauses, and does not split a sentence or add a `PauseItem`. A renderer that does not support the requested semantics must reject the request during preflight rather than silently ignore the control.
87
+
88
+ Do not store `A- R- T` in RenderPlan merely because one Kokoro frontend prefers that spelling; an engine may choose such a frontend-specific realization internally.
89
+
70
90
  Bindings copied into plans must be self-contained enough to execute without reopening the RenderProfile. This contract does not define renderer-specific path keys or local-asset resolution.
71
91
  This schema has no generic asset-reference type yet. A VoiceRender profile producer must refuse bindings that depend on profile-local or otherwise uncopied asset state; do not introduce renderer-specific asset-path keys into this shared contract.
72
92
 
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.1.4] - Unreleased
4
+
5
+ ### Added
6
+
7
+ - Added portable character interpretation semantics to speech controls
8
+
9
+ <!-- generated by releaseledger -->
10
+
11
+ ## [v0.1.3] - 2026-10-25
12
+
13
+ ### Added
14
+
15
+ - Added profile loading for re-identification while preserving validation of other profile data
16
+
17
+ <!-- generated by releaseledger -->
18
+
3
19
  ## [v0.1.2] - 2026-10-14
4
20
 
5
21
  ### Changed
@@ -4,6 +4,7 @@ from .codec import (
4
4
  load_plan,
5
5
  load_preflight,
6
6
  load_profile,
7
+ load_profile_for_reidentification,
7
8
  save_manifest,
8
9
  save_plan,
9
10
  save_preflight,
@@ -23,7 +24,15 @@ from .collection import (
23
24
  validate_renderplan_set,
24
25
  validate_renderplan_set_profile,
25
26
  )
26
- from .contracts import CONTRACTS, supported_contracts
27
+ from .contracts import (
28
+ CONTRACTS,
29
+ CONTROL_EMPHASIS,
30
+ CONTROL_PRONUNCIATION,
31
+ CONTROL_PROSODY,
32
+ CONTROL_SAY_AS,
33
+ SAY_AS_CHARACTERS,
34
+ supported_contracts,
35
+ )
27
36
  from .errors import CodecError, RenderPlanError, ValidationError
28
37
  from .model import (
29
38
  MANIFEST_FORMAT,
@@ -65,12 +74,17 @@ from .version import __version__
65
74
 
66
75
  __all__ = [
67
76
  "CONTRACTS",
77
+ "CONTROL_EMPHASIS",
78
+ "CONTROL_PRONUNCIATION",
79
+ "CONTROL_PROSODY",
80
+ "CONTROL_SAY_AS",
68
81
  "MANIFEST_FORMAT",
69
82
  "PLAN_FORMAT",
70
83
  "PREFLIGHT_FORMAT",
71
84
  "PROFILE_FORMAT",
72
85
  "RENDERPLAN_SET_FORMAT",
73
86
  "RENDERPLAN_SET_VERSION",
87
+ "SAY_AS_CHARACTERS",
74
88
  "AudioItem",
75
89
  "CapacityConstraint",
76
90
  "CodecError",
@@ -102,6 +116,7 @@ __all__ = [
102
116
  "load_plan",
103
117
  "load_preflight",
104
118
  "load_profile",
119
+ "load_profile_for_reidentification",
105
120
  "load_renderplan_set",
106
121
  "renderplan_set_from_dict",
107
122
  "renderplan_set_to_dict",
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.2'
22
- __version_tuple__ = version_tuple = (0, 1, 2)
21
+ __version__ = version = '0.1.4'
22
+ __version_tuple__ = version_tuple = (0, 1, 4)
23
23
 
24
- __commit_id__ = commit_id = 'gc2981a645'
24
+ __commit_id__ = commit_id = 'ga434ece68'
@@ -649,6 +649,13 @@ def load_profile(path: str | Path) -> RenderProfile:
649
649
  return profile_from_dict(_read(path))
650
650
 
651
651
 
652
+ def load_profile_for_reidentification(path: str | Path) -> RenderProfile:
653
+ """Load and validate a profile while ignoring only its stored identity."""
654
+ raw = dict(_read(path))
655
+ raw["profile_id"] = None
656
+ return profile_from_dict(raw)
657
+
658
+
652
659
  def save_profile(profile: RenderProfile, path: str | Path) -> Path:
653
660
  return _write(path, profile_to_dict(profile.with_identity()))
654
661
 
@@ -25,9 +25,25 @@ CONTRACTS: Mapping[str, int] = MappingProxyType(
25
25
  )
26
26
 
27
27
 
28
+ CONTROL_PRONUNCIATION = "pronunciation"
29
+ CONTROL_PROSODY = "prosody"
30
+ CONTROL_EMPHASIS = "emphasis"
31
+ CONTROL_SAY_AS = "say_as"
32
+
33
+ SAY_AS_CHARACTERS = "characters"
34
+
35
+
28
36
  def supported_contracts() -> Mapping[str, int]:
29
37
  """Return the immutable mapping of supported document formats to versions."""
30
38
  return CONTRACTS
31
39
 
32
40
 
33
- __all__ = ["CONTRACTS", "supported_contracts"]
41
+ __all__ = [
42
+ "CONTRACTS",
43
+ "CONTROL_EMPHASIS",
44
+ "CONTROL_PRONUNCIATION",
45
+ "CONTROL_PROSODY",
46
+ "CONTROL_SAY_AS",
47
+ "SAY_AS_CHARACTERS",
48
+ "supported_contracts",
49
+ ]
@@ -247,6 +247,7 @@ class SpeechControl:
247
247
  raise ValidationError("control must satisfy 0 <= start < end")
248
248
  require_nonempty(self.name, "control.name")
249
249
  require_toml_value(self.params, "control.params")
250
+ validate_standard_control(self)
250
251
 
251
252
  def ordering_key(self) -> tuple[int, int, str, bytes]:
252
253
  return (self.start, self.end, self.name, canonical_json_bytes(self.params))
@@ -260,6 +261,28 @@ class SpeechControl:
260
261
  }
261
262
 
262
263
 
264
+ def validate_standard_control(control: SpeechControl) -> None:
265
+ """Validate parameters for known standard speech controls."""
266
+ if control.name != "say_as":
267
+ return
268
+
269
+ allowed = {"interpret_as", "format", "detail"}
270
+ unknown = set(control.params).difference(allowed)
271
+ if unknown:
272
+ raise ValidationError(
273
+ "say_as control has unsupported parameter(s): " + ", ".join(sorted(unknown))
274
+ )
275
+
276
+ interpret_as = control.params.get("interpret_as")
277
+ if not isinstance(interpret_as, str) or not interpret_as.strip():
278
+ raise ValidationError("say_as.interpret_as must be a non-empty string")
279
+
280
+ for name in ("format", "detail"):
281
+ value = control.params.get(name)
282
+ if value is not None and (not isinstance(value, str) or not value.strip()):
283
+ raise ValidationError(f"say_as.{name} must be a non-empty string when present")
284
+
285
+
263
286
  @dataclass(frozen=True, slots=True)
264
287
  class TokenBundle:
265
288
  """Dense token spans/pos/tag and sparse, token-indexed lemma/morph values."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderplan
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Engine-neutral render profile, plan, preflight, manifest, and collection contracts for speech pipelines.
5
5
  Classifier: Development Status :: 3 - Alpha
6
6
  Classifier: Programming Language :: Python :: 3
@@ -90,6 +90,26 @@ The destination start offset of source span `N` inside the request is the sum of
90
90
 
91
91
  `SpeechControl` is request-local and renderer-neutral: it carries `start`, `end`, `name`, and TOML-safe `params`. Initial semantic names include `pronunciation`, `prosody`, `emphasis`, `say_as`, and `extension:<name>`. Controls participate in `segment_id`; renderers decide support, and unsupported controls must fail preflight rather than be ignored. Control ranges may overlap only when the source semantics permit it. Engine-specific voice selection is resolved into the opaque logical role/binding; audio directives become `AudioItem`s.
92
92
 
93
+ ### Standard `say_as` semantics
94
+
95
+ `SpeechControl(name="say_as")` requires a non-empty `interpret_as` string and accepts optional non-empty `format` and `detail` strings. The standard value `interpret_as = "characters"` means: speak the covered text as a sequence of separately pronounced characters, preserving the surrounding request as one continuous speech request when the engine can do so. It is portable intent; RenderPlan does not dictate an engine's realization or restrict future `interpret_as` values.
96
+
97
+ For example, the following control covers exactly `A R T`:
98
+
99
+ ```toml
100
+ [[flow.control]]
101
+ start = 0
102
+ end = 5
103
+ name = "say_as"
104
+
105
+ [flow.control.params]
106
+ interpret_as = "characters"
107
+ ```
108
+
109
+ The exact-range invariant is `speech.text[0:5] == "A R T"`. The control does not rewrite the canonical request text, prescribe spaces, hyphens, commas, SSML, phonemes, or pauses, and does not split a sentence or add a `PauseItem`. A renderer that does not support the requested semantics must reject the request during preflight rather than silently ignore the control.
110
+
111
+ Do not store `A- R- T` in RenderPlan merely because one Kokoro frontend prefers that spelling; an engine may choose such a frontend-specific realization internally.
112
+
93
113
  Bindings copied into plans must be self-contained enough to execute without reopening the RenderProfile. This contract does not define renderer-specific path keys or local-asset resolution.
94
114
  This schema has no generic asset-reference type yet. A VoiceRender profile producer must refuse bindings that depend on profile-local or otherwise uncopied asset state; do not introduce renderer-specific asset-path keys into this shared contract.
95
115
 
@@ -27,6 +27,11 @@ pyproject.toml
27
27
  .ledger/releaseledger/data/ledgers/main/releases/v0.1.2/release.md
28
28
  .ledger/releaseledger/data/ledgers/main/releases/v0.1.2/audit/commit-audit.yaml
29
29
  .ledger/releaseledger/data/ledgers/main/releases/v0.1.2/entries/entry-0001.md
30
+ .ledger/releaseledger/data/ledgers/main/releases/v0.1.3/release.md
31
+ .ledger/releaseledger/data/ledgers/main/releases/v0.1.3/audit/commit-audit.yaml
32
+ .ledger/releaseledger/data/ledgers/main/releases/v0.1.3/entries/entry-0001.md
33
+ .ledger/releaseledger/data/ledgers/main/releases/v0.1.4/release.md
34
+ .ledger/releaseledger/data/ledgers/main/releases/v0.1.4/entries/entry-0001.md
30
35
  .ledger/releaseledger/work/v0.1.1/audit-decisions.yaml
31
36
  .ledger/releaseledger/work/v0.1.1/audit.yaml
32
37
  .ledger/releaseledger/work/v0.1.1/entries.yaml
@@ -40,6 +45,12 @@ pyproject.toml
40
45
  .ledger/releaseledger/work/v0.1.2/range.json
41
46
  .ledger/releaseledger/work/v0.1.2/evidence/f88173e.patch
42
47
  .ledger/releaseledger/work/v0.1.2/evidence/manifest.json
48
+ .ledger/releaseledger/work/v0.1.3/audit-decisions.yaml
49
+ .ledger/releaseledger/work/v0.1.3/audit.yaml
50
+ .ledger/releaseledger/work/v0.1.3/entries.yaml
51
+ .ledger/releaseledger/work/v0.1.3/range.json
52
+ .ledger/releaseledger/work/v0.1.3/evidence/8c9c937.patch
53
+ .ledger/releaseledger/work/v0.1.3/evidence/manifest.json
43
54
  .ledger/taskledger/.ledger-project.toml
44
55
  .ledger/taskledger/config.toml
45
56
  docs/Makefile
@@ -80,4 +91,5 @@ renderplan.egg-info/top_level.txt
80
91
  tests/test_collection.py
81
92
  tests/test_contracts.py
82
93
  tests/test_identity.py
94
+ tests/test_profile_reidentification.py
83
95
  tests/test_renderplan_set.py
@@ -24,6 +24,11 @@
24
24
  ".ledger/releaseledger/data/ledgers/main/releases/v0.1.2/audit/commit-audit.yaml",
25
25
  ".ledger/releaseledger/data/ledgers/main/releases/v0.1.2/entries/entry-0001.md",
26
26
  ".ledger/releaseledger/data/ledgers/main/releases/v0.1.2/release.md",
27
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.1.3/audit/commit-audit.yaml",
28
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.1.3/entries/entry-0001.md",
29
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.1.3/release.md",
30
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.1.4/entries/entry-0001.md",
31
+ ".ledger/releaseledger/data/ledgers/main/releases/v0.1.4/release.md",
27
32
  ".ledger/releaseledger/work/v0.1.1/audit-decisions.yaml",
28
33
  ".ledger/releaseledger/work/v0.1.1/audit.yaml",
29
34
  ".ledger/releaseledger/work/v0.1.1/entries.yaml",
@@ -37,6 +42,12 @@
37
42
  ".ledger/releaseledger/work/v0.1.2/evidence/f88173e.patch",
38
43
  ".ledger/releaseledger/work/v0.1.2/evidence/manifest.json",
39
44
  ".ledger/releaseledger/work/v0.1.2/range.json",
45
+ ".ledger/releaseledger/work/v0.1.3/audit-decisions.yaml",
46
+ ".ledger/releaseledger/work/v0.1.3/audit.yaml",
47
+ ".ledger/releaseledger/work/v0.1.3/entries.yaml",
48
+ ".ledger/releaseledger/work/v0.1.3/evidence/8c9c937.patch",
49
+ ".ledger/releaseledger/work/v0.1.3/evidence/manifest.json",
50
+ ".ledger/releaseledger/work/v0.1.3/range.json",
40
51
  ".ledger/releaseledger/write.lock",
41
52
  ".ledger/taskledger/.ledger-project.toml",
42
53
  ".ledger/taskledger/config.toml",
@@ -73,6 +84,7 @@
73
84
  "tests/test_collection.py",
74
85
  "tests/test_contracts.py",
75
86
  "tests/test_identity.py",
87
+ "tests/test_profile_reidentification.py",
76
88
  "tests/test_renderplan_set.py"
77
89
  ]
78
90
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.1.4",
3
+ "distance": 0,
4
+ "node": "ga434ece686229c4accf193c4fdc8dd4357039586",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-28"
8
+ }
@@ -9,6 +9,11 @@ import pytest
9
9
 
10
10
  from renderplan import (
11
11
  CONTRACTS,
12
+ CONTROL_EMPHASIS,
13
+ CONTROL_PRONUNCIATION,
14
+ CONTROL_PROSODY,
15
+ CONTROL_SAY_AS,
16
+ SAY_AS_CHARACTERS,
12
17
  AudioItem,
13
18
  CapacityConstraint,
14
19
  CodecError,
@@ -218,6 +223,83 @@ def test_renderplan_roundtrip_with_controls_and_generic_binding(tmp_path: Path)
218
223
  assert loaded.speech[0].controls[0].params == {"level": "moderate"}
219
224
 
220
225
 
226
+ @pytest.mark.parametrize(
227
+ "params",
228
+ [
229
+ {},
230
+ {"format": "letters"},
231
+ {"interpret_as": ""},
232
+ {"interpret_as": 1},
233
+ {"interpret_as": "characters", "format": ""},
234
+ {"interpret_as": "characters", "detail": False},
235
+ {"interpret_as": "characters", "detail": ""},
236
+ {"interpret_as": "characters", "unexpected": "value"},
237
+ ],
238
+ )
239
+ def test_say_as_control_rejects_invalid_standard_parameters(params: dict[str, Any]) -> None:
240
+ with pytest.raises(ValidationError, match="say_as"):
241
+ SpeechControl(0, 5, "say_as", params)
242
+
243
+
244
+ def test_say_as_standard_constants_and_future_interpretations() -> None:
245
+ assert CONTROL_SAY_AS == "say_as"
246
+ assert CONTROL_EMPHASIS == "emphasis"
247
+ assert CONTROL_PRONUNCIATION == "pronunciation"
248
+ assert CONTROL_PROSODY == "prosody"
249
+ assert SAY_AS_CHARACTERS == "characters"
250
+ control = SpeechControl(
251
+ 0,
252
+ 5,
253
+ CONTROL_SAY_AS,
254
+ {"interpret_as": "date", "format": "dd.mm.yyyy", "detail": "1"},
255
+ )
256
+ assert control.params["interpret_as"] == "date"
257
+
258
+
259
+ def test_say_as_characters_roundtrip_preserves_sentence_text_and_flow(tmp_path: Path) -> None:
260
+ text = "Because someone called A R T finally."
261
+ start = text.index("A R T")
262
+ control = SpeechControl(
263
+ start,
264
+ start + len("A R T"),
265
+ CONTROL_SAY_AS,
266
+ {"interpret_as": SAY_AS_CHARACTERS},
267
+ )
268
+ value = plan(speech(text=text, controls=(control,)))
269
+ path = tmp_path / "say-as.renderplan.toml"
270
+ save_plan(value, path)
271
+ loaded = load_plan(path)
272
+
273
+ assert len(loaded.flow) == 1
274
+ assert len(loaded.speech) == 1
275
+ restored = loaded.speech[0]
276
+ restored_control = restored.controls[0]
277
+ assert restored.text == text
278
+ assert restored.text[restored_control.start : restored_control.end] == "A R T"
279
+ assert restored_control.name == CONTROL_SAY_AS
280
+ assert restored_control.params["interpret_as"] == SAY_AS_CHARACTERS
281
+ assert restored.planning_level == "sentence"
282
+ assert not any(isinstance(item, PauseItem) for item in loaded.flow)
283
+ assert "A- R- T" not in path.read_text(encoding="utf-8")
284
+
285
+
286
+ @pytest.mark.parametrize(
287
+ ("text", "possessive"),
288
+ [
289
+ ("A R T's house is nice.", "'s"),
290
+ ("A R T’s house is nice.", "’s"),
291
+ ],
292
+ )
293
+ def test_say_as_control_excludes_possessive_suffix(text: str, possessive: str) -> None:
294
+ start = text.index("A R T")
295
+ end = start + len("A R T")
296
+ control = SpeechControl(start, end, CONTROL_SAY_AS, {"interpret_as": SAY_AS_CHARACTERS})
297
+ item = speech(text=text, controls=(control,))
298
+
299
+ assert item.text[control.start : control.end] == "A R T"
300
+ assert item.text[control.end :].startswith(possessive)
301
+
302
+
221
303
  def test_leading_pause_is_legal() -> None:
222
304
  item = speech()
223
305
  value = plan(flow=(PauseItem(120, "authored"), item))
@@ -7,6 +7,7 @@ from renderplan import (
7
7
  RenderProfile,
8
8
  RoleProfile,
9
9
  SourceSpan,
10
+ SpeechControl,
10
11
  SpeechItem,
11
12
  )
12
13
 
@@ -43,6 +44,23 @@ def test_speech_request_identity_excludes_number_and_provenance() -> None:
43
44
  assert first.segment_id == renumbered.segment_id == different_source.segment_id
44
45
 
45
46
 
47
+ def test_say_as_control_changes_speech_request_identity() -> None:
48
+ plain = replace(
49
+ speech(),
50
+ text="A R T",
51
+ source=(SourceSpan("f000000.s000", 0, 5),),
52
+ segment_id=None,
53
+ ).with_identity()
54
+ controlled = replace(
55
+ plain,
56
+ controls=(SpeechControl(0, 5, "say_as", {"interpret_as": "characters"}),),
57
+ segment_id=None,
58
+ ).with_identity()
59
+
60
+ assert plain.text == controlled.text == "A R T"
61
+ assert plain.segment_id != controlled.segment_id
62
+
63
+
46
64
  def test_pause_is_part_of_plan_identity() -> None:
47
65
  item = speech()
48
66
  p1 = RenderPlan(SOURCE, PROFILE_ID, (item, PauseItem(100, "sentence"))).with_identity()
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from renderplan import (
8
+ CodecError,
9
+ ValidationError,
10
+ load_profile,
11
+ load_profile_for_reidentification,
12
+ )
13
+
14
+
15
+ def test_reidentification_loader_ignores_only_stored_profile_id(tmp_path: Path) -> None:
16
+ source = Path(__file__).parents[1] / "examples/af_heart.renderprofile.toml"
17
+ content = source.read_text(encoding="utf-8")
18
+
19
+ # Replace the complete stored digest without relying on its current value.
20
+ lines = content.splitlines()
21
+ id_line = next(index for index, line in enumerate(lines) if line.startswith("profile_id ="))
22
+ lines[id_line] = 'profile_id = "sha256:' + "0" * 64 + '"'
23
+ path = tmp_path / "stale.renderprofile.toml"
24
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
25
+
26
+ with pytest.raises(ValidationError):
27
+ load_profile(path)
28
+
29
+ profile = load_profile_for_reidentification(path)
30
+ assert profile.profile_id is None
31
+ repaired = profile.with_identity()
32
+ assert repaired.profile_id == repaired.computed_id()
33
+
34
+
35
+ def test_reidentification_loader_still_rejects_malformed_toml(tmp_path: Path) -> None:
36
+ path = tmp_path / "invalid.renderprofile.toml"
37
+ path.write_text("not = [valid", encoding="utf-8")
38
+
39
+ with pytest.raises(CodecError):
40
+ load_profile_for_reidentification(path)
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.1.2",
3
- "distance": 0,
4
- "node": "gc2981a645889c974f2f7dc8b93ee8c3828655d9d",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-14"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes