ssot-core 0.2.16.dev1__tar.gz → 0.2.17.dev1__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 (115) hide show
  1. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/PKG-INFO +4 -3
  2. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/pyproject.toml +4 -3
  3. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/PKG-INFO +4 -3
  4. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/SOURCES.txt +1 -0
  5. ssot_core-0.2.17.dev1/src/ssot_core.egg-info/requires.txt +6 -0
  6. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/documents.py +67 -10
  7. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/profile_eval.py +12 -9
  8. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/status_sync.py +12 -7
  9. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/upgrade.py +31 -5
  10. ssot_core-0.2.17.dev1/src/ssot_registry/guards/feature_claims.py +57 -0
  11. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/__init__.py +8 -0
  12. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/boundary.py +1 -0
  13. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/claim.py +1 -0
  14. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/document.py +59 -4
  15. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/evidence.py +1 -0
  16. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/feature.py +1 -0
  17. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/issue.py +1 -0
  18. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/profile.py +1 -0
  19. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/registry.py +1 -1
  20. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/release.py +1 -0
  21. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/risk.py +1 -0
  22. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/schema_version.py +1 -1
  23. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/test.py +1 -0
  24. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/templates/registry.full.json +1 -1
  25. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/templates/registry.minimal.json +1 -1
  26. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/document_io.py +12 -4
  27. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/registry_lock.py +10 -2
  28. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/documents.py +14 -7
  29. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/structure.py +27 -0
  30. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/tiers.py +3 -0
  31. ssot_core-0.2.16.dev1/src/ssot_core.egg-info/requires.txt +0 -5
  32. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/README.md +0 -0
  33. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/setup.cfg +0 -0
  34. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/dependency_links.txt +0 -0
  35. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/top_level.txt +0 -0
  36. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/__init__.py +0 -0
  37. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/__main__.py +0 -0
  38. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/__init__.py +0 -0
  39. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/boundary.py +0 -0
  40. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/claims.py +0 -0
  41. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/entity_ops.py +0 -0
  42. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/evidence.py +0 -0
  43. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/graph.py +0 -0
  44. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/init.py +0 -0
  45. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/lifecycle.py +0 -0
  46. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/load.py +0 -0
  47. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/plan.py +0 -0
  48. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/profile_resolution.py +0 -0
  49. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/registry.py +0 -0
  50. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/release.py +0 -0
  51. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/save.py +0 -0
  52. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/test_execution.py +0 -0
  53. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/validate.py +0 -0
  54. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/__init__.py +0 -0
  55. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/adr_cmd.py +0 -0
  56. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/boundary_cmd.py +0 -0
  57. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/claim_cmd.py +0 -0
  58. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/common.py +0 -0
  59. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/evidence_cmd.py +0 -0
  60. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/feature_cmd.py +0 -0
  61. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/graph_cmd.py +0 -0
  62. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/init_cmd.py +0 -0
  63. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/issue_cmd.py +0 -0
  64. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/main.py +0 -0
  65. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/profile_cmd.py +0 -0
  66. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/registry_cmd.py +0 -0
  67. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/release_cmd.py +0 -0
  68. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/risk_cmd.py +0 -0
  69. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/spec_cmd.py +0 -0
  70. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/test_cmd.py +0 -0
  71. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/upgrade_cmd.py +0 -0
  72. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/validate_cmd.py +0 -0
  73. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/graph/__init__.py +0 -0
  74. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/graph/export_dot.py +0 -0
  75. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/graph/export_json.py +0 -0
  76. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/__init__.py +0 -0
  77. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/certification.py +0 -0
  78. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/claim_closure.py +0 -0
  79. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/document_lifecycle.py +0 -0
  80. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/document_supersession.py +0 -0
  81. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/feature_requirements.py +0 -0
  82. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/lifecycle.py +0 -0
  83. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/profile_requirements.py +0 -0
  84. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/promotion.py +0 -0
  85. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/publication.py +0 -0
  86. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/enums.py +0 -0
  87. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/ids.py +0 -0
  88. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/__init__.py +0 -0
  89. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/certification_report.py +0 -0
  90. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/summary.py +0 -0
  91. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/validation_report.py +0 -0
  92. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/__init__.py +0 -0
  93. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/boundary_snapshot.py +0 -0
  94. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/hashing.py +0 -0
  95. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/published_snapshot.py +0 -0
  96. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/release_snapshot.py +0 -0
  97. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/templates/__init__.py +0 -0
  98. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/__init__.py +0 -0
  99. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/errors.py +0 -0
  100. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/formatting.py +0 -0
  101. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/fs.py +0 -0
  102. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/jcs.py +0 -0
  103. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/jsonio.py +0 -0
  104. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/time.py +0 -0
  105. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/__init__.py +0 -0
  106. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/bidirectional.py +0 -0
  107. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/bounds.py +0 -0
  108. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/coverage.py +0 -0
  109. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/filesystem.py +0 -0
  110. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/identity.py +0 -0
  111. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/lifecycle.py +0 -0
  112. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/promotion.py +0 -0
  113. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/references.py +0 -0
  114. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/reservations.py +0 -0
  115. {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssot-core
3
- Version: 0.2.16.dev1
3
+ Version: 0.2.17.dev1
4
4
  Summary: Core Python runtime, registry model, validation, and release workflow APIs for SSOT.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License-Expression: Apache-2.0
@@ -34,8 +34,9 @@ Classifier: Topic :: System :: Archiving
34
34
  Classifier: Topic :: Utilities
35
35
  Requires-Python: <3.14,>=3.10
36
36
  Description-Content-Type: text/markdown
37
- Requires-Dist: ssot-contracts==0.2.16.dev1
38
- Requires-Dist: ssot-views==0.2.16.dev1
37
+ Requires-Dist: orjson<4.0,>=3.10
38
+ Requires-Dist: ssot-contracts==0.2.17.dev1
39
+ Requires-Dist: ssot-views==0.2.17.dev1
39
40
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
40
41
 
41
42
  <div align="center">
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ssot-core"
7
- version = "0.2.16.dev1"
7
+ version = "0.2.17.dev1"
8
8
  description = "Core Python runtime, registry model, validation, and release workflow APIs for SSOT."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<3.14"
11
11
  license = "Apache-2.0"
12
12
  authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
13
13
  dependencies = [
14
- "ssot-contracts==0.2.16.dev1",
15
- "ssot-views==0.2.16.dev1",
14
+ "orjson>=3.10,<4.0",
15
+ "ssot-contracts==0.2.17.dev1",
16
+ "ssot-views==0.2.17.dev1",
16
17
  "tomli>=2.0.1; python_version < '3.11'",
17
18
  ]
18
19
  keywords = ["ssot", "core", "registry", "validation", "release-management", "governance", "compliance"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssot-core
3
- Version: 0.2.16.dev1
3
+ Version: 0.2.17.dev1
4
4
  Summary: Core Python runtime, registry model, validation, and release workflow APIs for SSOT.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License-Expression: Apache-2.0
@@ -34,8 +34,9 @@ Classifier: Topic :: System :: Archiving
34
34
  Classifier: Topic :: Utilities
35
35
  Requires-Python: <3.14,>=3.10
36
36
  Description-Content-Type: text/markdown
37
- Requires-Dist: ssot-contracts==0.2.16.dev1
38
- Requires-Dist: ssot-views==0.2.16.dev1
37
+ Requires-Dist: orjson<4.0,>=3.10
38
+ Requires-Dist: ssot-contracts==0.2.17.dev1
39
+ Requires-Dist: ssot-views==0.2.17.dev1
39
40
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
40
41
 
41
42
  <div align="center">
@@ -55,6 +55,7 @@ src/ssot_registry/guards/certification.py
55
55
  src/ssot_registry/guards/claim_closure.py
56
56
  src/ssot_registry/guards/document_lifecycle.py
57
57
  src/ssot_registry/guards/document_supersession.py
58
+ src/ssot_registry/guards/feature_claims.py
58
59
  src/ssot_registry/guards/feature_requirements.py
59
60
  src/ssot_registry/guards/lifecycle.py
60
61
  src/ssot_registry/guards/profile_requirements.py
@@ -0,0 +1,6 @@
1
+ orjson<4.0,>=3.10
2
+ ssot-contracts==0.2.17.dev1
3
+ ssot-views==0.2.17.dev1
4
+
5
+ [:python_version < "3.11"]
6
+ tomli>=2.0.1
@@ -12,14 +12,17 @@ from ssot_registry.guards.document_lifecycle import (
12
12
  )
13
13
  from ssot_registry.guards.document_supersession import apply_supersession
14
14
  from ssot_registry.model.document import (
15
+ DOCUMENT_ORIGINS,
15
16
  DOCUMENT_STATUSES,
16
17
  DOCUMENT_SLUG_PATTERN,
18
+ EXTENSION_PACK_RESERVATION_PREFIX,
17
19
  SPEC_KINDS,
18
20
  build_document_path,
19
21
  default_document_id_reservations,
22
+ is_extension_pack_reservation_owner,
20
23
  load_document_manifest as load_packaged_document_manifest,
21
24
  normalize_document_id,
22
- read_packaged_document_bytes,
25
+ read_manifest_document_bytes,
23
26
  read_packaged_document_text,
24
27
  reservation_kind_key,
25
28
  section_for_document_kind,
@@ -199,6 +202,53 @@ def _manifest_row_to_registry_row(registry: dict[str, Any], kind: str, manifest_
199
202
  return row
200
203
 
201
204
 
205
+ def _packaged_manifest_entries(kind: str) -> list[dict[str, Any]]:
206
+ manifest_entries = load_packaged_document_manifest(kind, include_untrusted=True)
207
+ untrusted_extension_catalogs = sorted(
208
+ {
209
+ str(entry.get("catalog_id", ""))
210
+ for entry in manifest_entries
211
+ if entry.get("origin") == "extension-pack" and not bool(entry.get("trusted", False))
212
+ }
213
+ )
214
+ if untrusted_extension_catalogs:
215
+ raise ValidationError(
216
+ "Untrusted extension-pack catalogs are available but not allowed for sync: "
217
+ + ", ".join(untrusted_extension_catalogs)
218
+ )
219
+
220
+ trusted_entries = [entry for entry in manifest_entries if bool(entry.get("trusted", False))]
221
+ seen_ids: dict[str, str] = {}
222
+ seen_numbers: dict[int, str] = {}
223
+ seen_paths: dict[str, str] = {}
224
+ for entry in trusted_entries:
225
+ entry_id = str(entry.get("id", ""))
226
+ catalog_id = str(entry.get("catalog_id", ""))
227
+ number = entry.get("number")
228
+ target_path = str(entry.get("target_path", ""))
229
+ origin = entry.get("origin")
230
+ if origin not in {"ssot-origin", "extension-pack"}:
231
+ raise ValidationError(f"Packaged {kind} manifest entry {entry_id} must use origin 'ssot-origin' or 'extension-pack'")
232
+ if entry_id in seen_ids:
233
+ raise ValidationError(
234
+ f"Packaged {kind} id conflict: {entry_id} is present in both {seen_ids[entry_id]} and {catalog_id}"
235
+ )
236
+ if isinstance(number, int) and number in seen_numbers:
237
+ raise ValidationError(
238
+ f"Packaged {kind} number conflict: {number:04d} is present in both {seen_numbers[number]} and {catalog_id}"
239
+ )
240
+ if target_path and target_path in seen_paths:
241
+ raise ValidationError(
242
+ f"Packaged {kind} target_path conflict: {target_path} is present in both {seen_paths[target_path]} and {catalog_id}"
243
+ )
244
+ seen_ids[entry_id] = catalog_id
245
+ if isinstance(number, int):
246
+ seen_numbers[number] = catalog_id
247
+ if target_path:
248
+ seen_paths[target_path] = catalog_id
249
+ return trusted_entries
250
+
251
+
202
252
  def _existing_numbers(registry: dict[str, Any], kind: str) -> set[int]:
203
253
  return {
204
254
  row["number"]
@@ -318,7 +368,7 @@ def create_document(
318
368
  spec_kind: str | None = None,
319
369
  adr_ids: list[str] | None = None,
320
370
  ) -> dict[str, Any]:
321
- if origin not in {"repo-local", "ssot-core", "ssot-origin"}:
371
+ if origin not in DOCUMENT_ORIGINS:
322
372
  raise ValidationError(f"Unsupported origin '{origin}' for {_document_label(kind)} creation")
323
373
  if not isinstance(title, str) or not title.strip():
324
374
  raise ValidationError(f"{_document_label(kind)} title must be a non-empty string")
@@ -335,24 +385,33 @@ def create_document(
335
385
  repo_kind = _repo_kind(registry)
336
386
  if repo_kind == "repo-local" and origin != "repo-local":
337
387
  raise ValidationError(f"Repo-local repositories may only create repo-local {_document_label(kind)} rows")
338
- if repo_kind in {"ssot-core", "ssot-origin"} and origin == "repo-local":
388
+ if repo_kind in {"ssot-core", "ssot-origin"} and origin not in {"ssot-core", "ssot-origin"}:
339
389
  raise ValidationError(f"{repo_kind} repositories may only create ssot-core or ssot-origin {_document_label(kind)} rows")
390
+ if repo_kind == "extension-pack" and origin != "extension-pack":
391
+ raise ValidationError(f"extension-pack repositories may only create extension-pack {_document_label(kind)} rows")
340
392
  if number is None:
341
393
  if repo_kind in {"ssot-core", "ssot-origin"} and origin in {"ssot-core", "ssot-origin"}:
342
394
  raise ValidationError(f"{repo_kind} {_document_label(kind)} creation for {origin} requires an explicit --number")
395
+ if repo_kind == "extension-pack" and origin == "extension-pack":
396
+ raise ValidationError(f"extension-pack {_document_label(kind)} creation for {origin} requires an explicit --number")
343
397
  number = _allocate_number(registry, kind, reserve_range)
344
398
  else:
345
399
  _ensure_assignable_number(
346
400
  registry,
347
401
  kind,
348
402
  number,
349
- allow_non_assignable=repo_kind in {"ssot-core", "ssot-origin"} and origin in {"ssot-core", "ssot-origin"},
403
+ allow_non_assignable=repo_kind in {"ssot-core", "ssot-origin", "extension-pack"}
404
+ and origin in {"ssot-core", "ssot-origin", "extension-pack"},
350
405
  )
351
406
  reservation = _reservation_for_number(registry, kind, number)
352
407
  if origin in {"ssot-core", "ssot-origin"} and reservation.get("owner") != origin:
353
408
  raise ValidationError(
354
409
  f"{_document_label(kind)} number {number} must use reservation owned by {origin}; got {reservation.get('owner')}"
355
410
  )
411
+ if origin == "extension-pack" and not is_extension_pack_reservation_owner(reservation.get("owner")):
412
+ raise ValidationError(
413
+ f"{_document_label(kind)} number {number} must use reservation owned by {EXTENSION_PACK_RESERVATION_PREFIX}<catalog-id>; got {reservation.get('owner')}"
414
+ )
356
415
 
357
416
  authored_payload = _resolve_authored_payload(kind, title=title, body=body, body_file=body_file, require_one=True)
358
417
  authored_payload = _apply_spec_adr_ids(authored_payload, adr_ids) if kind == "spec" else authored_payload
@@ -596,7 +655,7 @@ def _sync_manifest_document(
596
655
  raise ValidationError(
597
656
  f"Cannot sync {document_id}; number {expected_row['number']} is already used by {row.get('id')}"
598
657
  )
599
- payload = read_packaged_document_bytes(kind, manifest_entry["filename"])
658
+ payload = read_manifest_document_bytes(kind, manifest_entry)
600
659
  target = repo_root / expected_row["path"]
601
660
  target.parent.mkdir(parents=True, exist_ok=True)
602
661
  target.write_bytes(payload)
@@ -610,7 +669,7 @@ def _sync_manifest_document(
610
669
  f"Cannot sync {document_id}; field {field_name} was locally modified from {expected_row.get(field_name)} to {current.get(field_name)}"
611
670
  )
612
671
 
613
- payload = read_packaged_document_bytes(kind, manifest_entry["filename"])
672
+ payload = read_manifest_document_bytes(kind, manifest_entry)
614
673
  target = repo_root / expected_row["path"]
615
674
  target.parent.mkdir(parents=True, exist_ok=True)
616
675
  actual_hash = sha256_normalized_text_path(target) if target.exists() else None
@@ -641,9 +700,7 @@ def sync_documents_in_memory(registry: dict[str, Any], repo_root: Path, kind: st
641
700
  lookup = _row_lookup(registry, kind)
642
701
  summary: dict[str, list[str]] = {"created": [], "updated": [], "unchanged": []}
643
702
 
644
- for manifest_entry in load_packaged_document_manifest(kind):
645
- if manifest_entry.get("origin") != "ssot-origin":
646
- raise ValidationError(f"Packaged {kind} manifest entry {manifest_entry.get('id')} must use origin 'ssot-origin'")
703
+ for manifest_entry in _packaged_manifest_entries(kind):
647
704
  if not schema_version_meets_minimum(registry.get("schema_version", 0), manifest_entry.get("minimum_schema_version", 0)):
648
705
  continue
649
706
  outcome, document_id = _sync_manifest_document(registry, repo_root, kind, manifest_entry, lookup)
@@ -658,7 +715,7 @@ def sync_documents_in_memory(registry: dict[str, Any], repo_root: Path, kind: st
658
715
  def sync_documents(path: str | Path, kind: str) -> dict[str, Any]:
659
716
  registry_path, repo_root, registry = load_registry(path)
660
717
  section = section_for_document_kind(kind)
661
- if _repo_kind(registry) in {"ssot-core", "ssot-origin"}:
718
+ if _repo_kind(registry) in {"ssot-core", "ssot-origin", "extension-pack"}:
662
719
  return {
663
720
  "passed": True,
664
721
  "registry_path": registry_path.as_posix(),
@@ -3,6 +3,10 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  from ssot_registry.guards.claim_closure import evaluate_claim_guard
6
+ from ssot_registry.guards.feature_claims import (
7
+ active_required_feature_claims,
8
+ feature_claim_ceiling_failures,
9
+ )
6
10
  from ssot_registry.guards.feature_requirements import evaluate_required_feature_failures
7
11
  from ssot_registry.model.enums import CLAIM_TIER_RANK
8
12
  from ssot_registry.validators.identity import build_index
@@ -42,24 +46,23 @@ def evaluate_feature_passing(
42
46
 
43
47
  checked_claim_ids: list[str] = []
44
48
  satisfying_claim_ids: list[str] = []
45
- for claim_id in feature.get("claim_ids", []):
46
- if claim_id not in index["claims"]:
47
- continue
49
+ active_claims = active_required_feature_claims(feature, index)
50
+ for claim in active_claims:
51
+ claim_id = str(claim["id"])
48
52
  checked_claim_ids.append(claim_id)
49
- claim = index["claims"][claim_id]
50
53
  guard = evaluate_claim_guard(claim, index, guard_policies)
51
54
  if not guard.get("passed"):
52
55
  continue
53
- if required_tier is not None and CLAIM_TIER_RANK[claim["tier"]] < CLAIM_TIER_RANK[required_tier]:
54
- continue
55
- satisfying_claim_ids.append(claim_id)
56
+ if claim.get("tier") != "T0" and (required_tier is None or CLAIM_TIER_RANK[claim["tier"]] >= CLAIM_TIER_RANK[required_tier]):
57
+ satisfying_claim_ids.append(claim_id)
56
58
 
57
- checks["claim_target_met"] = bool(satisfying_claim_ids) if required_tier is not None else bool(checked_claim_ids)
59
+ checks["claim_target_met"] = bool(active_claims) and len(satisfying_claim_ids) == len(active_claims)
58
60
  if not checks["claim_target_met"]:
59
61
  if required_tier is None:
60
62
  failures.append(f"Feature {feature_id} has no effective required claim tier")
61
63
  else:
62
- failures.append(f"Feature {feature_id} has no satisfying claim at or above tier {required_tier}")
64
+ failures.append(f"Feature {feature_id} does not have all active required claims satisfying tier {required_tier}")
65
+ failures.extend(failure.removeprefix(f"features.{feature_id} ") for failure in feature_claim_ceiling_failures(feature, index))
63
66
 
64
67
  return {
65
68
  "feature_id": feature_id,
@@ -5,6 +5,11 @@ from pathlib import Path
5
5
  from typing import Any
6
6
 
7
7
  from ssot_registry.guards.claim_closure import evaluate_claim_guard
8
+ from ssot_registry.guards.feature_claims import (
9
+ active_required_feature_claims,
10
+ claim_satisfies_feature_implementation,
11
+ feature_claim_ceiling_failures,
12
+ )
8
13
  from ssot_registry.model.enums import CLAIM_STATUS_RANK, CLAIM_TIER_RANK
9
14
  from ssot_registry.validators.identity import build_index
10
15
 
@@ -184,9 +189,7 @@ def _sync_claims(registry: dict[str, Any]) -> list[dict[str, object]]:
184
189
 
185
190
 
186
191
  def _claim_satisfies_feature(claim: dict[str, Any], required_tier: str | None) -> bool:
187
- if required_tier is not None and CLAIM_TIER_RANK[claim["tier"]] < CLAIM_TIER_RANK[required_tier]:
188
- return False
189
- return CLAIM_STATUS_RANK.get(claim.get("status"), -999) >= CLAIM_STATUS_RANK["evidenced"]
192
+ return claim_satisfies_feature_implementation(claim, required_tier)
190
193
 
191
194
 
192
195
  def _sync_features_once(registry: dict[str, Any]) -> list[dict[str, object]]:
@@ -203,25 +206,27 @@ def _sync_features_once(registry: dict[str, Any]) -> list[dict[str, object]]:
203
206
  else:
204
207
  linked_tests = [index["tests"][test_id] for test_id in feature.get("test_ids", []) if test_id in index["tests"]]
205
208
  linked_claims = [index["claims"][claim_id] for claim_id in feature.get("claim_ids", []) if claim_id in index["claims"]]
209
+ active_claims = active_required_feature_claims(feature, index)
206
210
  required_features = [index["features"][required_id] for required_id in feature.get("requires", []) if required_id in index["features"]]
207
211
  required_tier = feature.get("plan", {}).get("target_claim_tier")
208
212
  tests_pass = bool(linked_tests) and all(test.get("status") == "passing" for test in linked_tests)
209
- claims_pass = any(_claim_satisfies_feature(claim, required_tier) for claim in linked_claims)
213
+ claims_pass = bool(active_claims) and all(_claim_satisfies_feature(claim, required_tier) for claim in active_claims)
210
214
  requirements_pass = all(required.get("implementation_status") == "implemented" for required in required_features)
211
215
  only_planned_support = (
212
216
  bool(linked_tests or linked_claims)
213
217
  and all(test.get("status") == "planned" for test in linked_tests)
214
- and all(CLAIM_STATUS_RANK.get(claim.get("status"), -999) <= CLAIM_STATUS_RANK["proposed"] for claim in linked_claims)
218
+ and all(CLAIM_STATUS_RANK.get(claim.get("status"), -999) <= CLAIM_STATUS_RANK["proposed"] for claim in active_claims)
215
219
  )
216
220
  if tests_pass and claims_pass and requirements_pass:
217
221
  status = "implemented"
218
- reason = "feature has passing tests, satisfying claims, and implemented requirements"
222
+ reason = "feature has passing tests, all active required claims satisfy implementation, and implemented requirements"
219
223
  elif only_planned_support:
220
224
  status = "absent"
221
225
  reason = "feature has only planned verification support"
222
226
  elif linked_tests or linked_claims or required_features:
223
227
  status = "partial"
224
- reason = "feature has linked support but does not yet satisfy implementation criteria"
228
+ ceiling_failures = feature_claim_ceiling_failures(feature, index)
229
+ reason = "; ".join(ceiling_failures) if ceiling_failures else "feature has linked support but does not yet satisfy implementation criteria"
225
230
  else:
226
231
  status = "absent"
227
232
  reason = "feature has no linked implementation support"
@@ -37,6 +37,7 @@ LEGACY_V3_VERSION = "0.1.2"
37
37
  SSOT_ORIGIN_V8_OFFSET = 599
38
38
  SCHEMA_V0_1_0 = "0.1.0"
39
39
  SCHEMA_V0_2_0 = "0.2.0"
40
+ SCHEMA_V0_3_0 = "0.3.0"
40
41
  MIGRATION_RELEASE_WINDOWS = {
41
42
  (3, 4): "0.1.x->0.2.1",
42
43
  (4, 5): "0.2.1->0.2.2",
@@ -47,7 +48,8 @@ MIGRATION_RELEASE_WINDOWS = {
47
48
  (9, 10): "0.2.6->0.2.7",
48
49
  (10, SCHEMA_V0_1_0): "0.2.7->0.2.7",
49
50
  (SCHEMA_V0_1_0, SCHEMA_V0_2_0): "0.2.10->0.2.10",
50
- (SCHEMA_V0_2_0, "0.3.0"): "0.2.10->0.3.0",
51
+ (SCHEMA_V0_2_0, SCHEMA_V0_3_0): "0.2.10->0.3.0",
52
+ (SCHEMA_V0_3_0, SCHEMA_VERSION): "0.2.10->0.4.0",
51
53
  }
52
54
 
53
55
  MIGRATION_PATHS = (
@@ -60,7 +62,8 @@ MIGRATION_PATHS = (
60
62
  (9, 10, "migrate_v9_to_v10"),
61
63
  (10, SCHEMA_V0_1_0, "migrate_v10_to_v0_1_0"),
62
64
  (SCHEMA_V0_1_0, SCHEMA_V0_2_0, "migrate_v0_1_0_to_v0_2_0"),
63
- (SCHEMA_V0_2_0, SCHEMA_VERSION, "migrate_v0_2_0_to_v0_3_0"),
65
+ (SCHEMA_V0_2_0, SCHEMA_V0_3_0, "migrate_v0_2_0_to_v0_3_0"),
66
+ (SCHEMA_V0_3_0, SCHEMA_VERSION, "migrate_v0_3_0_to_v0_4_0"),
64
67
  )
65
68
 
66
69
 
@@ -527,11 +530,24 @@ def migrate_v0_2_0_to_v0_3_0(
527
530
  ) -> dict[str, Any]:
528
531
  _ = repo_root, previous_version, target_version
529
532
  migrated = deepcopy(registry)
530
- migrated["schema_version"] = SCHEMA_VERSION
533
+ migrated["schema_version"] = SCHEMA_V0_3_0
531
534
  _seed_release_boundary_ids(migrated)
532
535
  return migrated
533
536
 
534
537
 
538
+ def migrate_v0_3_0_to_v0_4_0(
539
+ registry: dict[str, Any],
540
+ repo_root: Path,
541
+ *,
542
+ previous_version: str,
543
+ target_version: str,
544
+ ) -> dict[str, Any]:
545
+ _ = repo_root, previous_version, target_version
546
+ migrated = deepcopy(registry)
547
+ migrated["schema_version"] = SCHEMA_VERSION
548
+ return migrated
549
+
550
+
535
551
  def target_version_from_registry(registry: dict[str, Any]) -> str:
536
552
  tooling = registry.get("tooling")
537
553
  if isinstance(tooling, dict):
@@ -668,11 +684,21 @@ def upgrade_registry(
668
684
  target_version=target_version,
669
685
  )
670
686
  schema_migrations.append("migrate_v0_2_0_to_v0_3_0")
671
- migrations.append(_migration_window_label(SCHEMA_V0_2_0, SCHEMA_VERSION))
687
+ migrations.append(_migration_window_label(SCHEMA_V0_2_0, SCHEMA_V0_3_0))
688
+ source_schema = SCHEMA_V0_3_0
689
+ if source_schema == SCHEMA_V0_3_0:
690
+ working = migrate_v0_3_0_to_v0_4_0(
691
+ working,
692
+ repo_root,
693
+ previous_version=source_tooling_version,
694
+ target_version=target_version,
695
+ )
696
+ schema_migrations.append("migrate_v0_3_0_to_v0_4_0")
697
+ migrations.append(_migration_window_label(SCHEMA_V0_3_0, SCHEMA_VERSION))
672
698
  source_schema = SCHEMA_VERSION
673
699
  elif source_schema != SCHEMA_VERSION:
674
700
  raise RegistryError(
675
- f"Unsupported registry schema_version {source_schema}; expected 3, 4, 5, 6, 7, 8, 9, 10, 0.1.0, 0.2.0 or {SCHEMA_VERSION}"
701
+ f"Unsupported registry schema_version {source_schema}; expected 3, 4, 5, 6, 7, 8, 9, 10, 0.1.0, 0.2.0, 0.3.0 or {SCHEMA_VERSION}"
676
702
  )
677
703
 
678
704
  normalized_current = _normalize_current_registry(working)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ssot_registry.model.enums import CLAIM_STATUS_RANK, CLAIM_TIER_RANK
6
+
7
+ INACTIVE_CLAIM_STATUSES = {"retired"}
8
+
9
+
10
+ def active_required_feature_claims(
11
+ feature: dict[str, Any],
12
+ index: dict[str, dict[str, dict[str, object]]],
13
+ ) -> list[dict[str, object]]:
14
+ return [
15
+ index["claims"][claim_id]
16
+ for claim_id in feature.get("claim_ids", [])
17
+ if claim_id in index["claims"] and index["claims"][claim_id].get("status") not in INACTIVE_CLAIM_STATUSES
18
+ ]
19
+
20
+
21
+ def claim_satisfies_feature_implementation(
22
+ claim: dict[str, object],
23
+ required_tier: str | None,
24
+ ) -> bool:
25
+ if claim.get("tier") == "T0":
26
+ return False
27
+ if required_tier is not None and CLAIM_TIER_RANK[claim["tier"]] < CLAIM_TIER_RANK[required_tier]:
28
+ return False
29
+ return CLAIM_STATUS_RANK.get(claim.get("status"), -999) >= CLAIM_STATUS_RANK["evidenced"]
30
+
31
+
32
+ def feature_claim_ceiling_failures(
33
+ feature: dict[str, Any],
34
+ index: dict[str, dict[str, dict[str, object]]],
35
+ *,
36
+ require_evidenced_status: bool = True,
37
+ ) -> list[str]:
38
+ feature_id = str(feature["id"])
39
+ required_tier = feature.get("plan", {}).get("target_claim_tier")
40
+ failures: list[str] = []
41
+ for claim in active_required_feature_claims(feature, index):
42
+ claim_id = str(claim["id"])
43
+ claim_tier = str(claim["tier"])
44
+ claim_status = str(claim.get("status"))
45
+ if claim_tier == "T0":
46
+ failures.append(f"features.{feature_id} is capped below implemented by active T0 claim {claim_id}")
47
+ continue
48
+ if required_tier is not None and CLAIM_TIER_RANK[claim_tier] < CLAIM_TIER_RANK[required_tier]:
49
+ failures.append(
50
+ f"features.{feature_id} is capped below implemented by claim {claim_id} tier {claim_tier} below target {required_tier}"
51
+ )
52
+ continue
53
+ if require_evidenced_status and CLAIM_STATUS_RANK.get(claim_status, -999) < CLAIM_STATUS_RANK["evidenced"]:
54
+ failures.append(
55
+ f"features.{feature_id} is capped below implemented by claim {claim_id} status {claim_status} below evidenced"
56
+ )
57
+ return failures
@@ -12,10 +12,14 @@ from .document import (
12
12
  document_path_has_supported_suffix,
13
13
  document_path_variants,
14
14
  default_document_id_reservations,
15
+ extension_pack_reservation_owner,
15
16
  format_document_number,
17
+ is_extension_pack_reservation_owner,
16
18
  load_document_manifest,
17
19
  normalize_document_id,
18
20
  parse_document_filename,
21
+ read_manifest_document_bytes,
22
+ read_manifest_document_text,
19
23
  read_packaged_document_bytes,
20
24
  read_packaged_document_text,
21
25
  )
@@ -39,15 +43,19 @@ __all__ = [
39
43
  "build_minimal_registry",
40
44
  "count_entities",
41
45
  "default_document_id_reservations",
46
+ "extension_pack_reservation_owner",
42
47
  "default_guard_policies",
43
48
  "default_paths",
44
49
  "filesystem_safe_id",
45
50
  "format_document_number",
51
+ "is_extension_pack_reservation_owner",
46
52
  "is_normalized_id",
47
53
  "load_document_manifest",
48
54
  "normalize_document_id",
49
55
  "parse_document_filename",
50
56
  "ProfileRow",
57
+ "read_manifest_document_bytes",
58
+ "read_manifest_document_text",
51
59
  "read_packaged_document_bytes",
52
60
  "read_packaged_document_text",
53
61
  ]
@@ -6,6 +6,7 @@ from typing import TypedDict
6
6
  class BoundaryRow(TypedDict, total=False):
7
7
  id: str
8
8
  title: str
9
+ body: str
9
10
  status: str
10
11
  frozen: bool
11
12
  feature_ids: list[str]
@@ -6,6 +6,7 @@ from typing import TypedDict
6
6
  class ClaimRow(TypedDict, total=False):
7
7
  id: str
8
8
  title: str
9
+ body: str
9
10
  status: str
10
11
  tier: str
11
12
  kind: str
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from pathlib import Path
5
- from typing import Any, TypedDict
5
+ from typing import Any, Callable, TypedDict
6
6
 
7
7
  from ssot_contracts import load_document_manifest as contracts_load_document_manifest
8
8
  from ssot_contracts import read_packaged_document_bytes as contracts_read_packaged_document_bytes
@@ -17,7 +17,8 @@ DOCUMENT_FILENAME_PREFIXES = CONTRACT_DATA["document_contract"]["filename_prefix
17
17
  DOCUMENT_PATH_KEYS = CONTRACT_DATA["document_contract"]["path_keys"]
18
18
  DOCUMENT_RESERVATION_KEYS = CONTRACT_DATA["document_contract"]["reservation_keys"]
19
19
  DOCUMENT_FILE_SUFFIXES = (".json", ".yaml")
20
- DOCUMENT_ORIGINS = {"ssot-core", "ssot-origin", "repo-local"}
20
+ DOCUMENT_ORIGINS = {"ssot-core", "ssot-origin", "repo-local", "extension-pack"}
21
+ EXTENSION_PACK_RESERVATION_PREFIX = "extension-pack:"
21
22
  DOCUMENT_STATUSES = tuple(CONTRACT_DATA["choice_sets"]["document_statuses"])
22
23
  CREATE_ALLOWED_STATUSES = ("draft", "in_review", "accepted", "rejected", "withdrawn")
23
24
  TERMINAL_STATUSES = ("rejected", "withdrawn", "superseded", "retired")
@@ -65,6 +66,14 @@ class SpecRow(TypedDict, total=False):
65
66
  status_notes: list[StatusNote]
66
67
 
67
68
 
69
+ class DocumentCatalogProvider(TypedDict):
70
+ catalog_id: str
71
+ trusted_by_default: bool
72
+ load_manifest: Callable[[str], list[dict[str, Any]]]
73
+ read_text: Callable[[str, str], str]
74
+ read_bytes: Callable[[str, str], bytes]
75
+
76
+
68
77
  def format_document_number(number: int) -> str:
69
78
  return f"{number:04d}"
70
79
 
@@ -114,8 +123,54 @@ def default_document_id_reservations() -> dict[str, list[dict[str, Any]]]:
114
123
  }
115
124
 
116
125
 
117
- def load_document_manifest(kind: str) -> list[dict[str, Any]]:
118
- return contracts_load_document_manifest(kind)
126
+ _DOCUMENT_CATALOG_PROVIDERS: list[DocumentCatalogProvider] = [
127
+ {
128
+ "catalog_id": "ssot-origin",
129
+ "trusted_by_default": True,
130
+ "load_manifest": contracts_load_document_manifest,
131
+ "read_text": contracts_read_packaged_document_text,
132
+ "read_bytes": contracts_read_packaged_document_bytes,
133
+ }
134
+ ]
135
+
136
+
137
+ def extension_pack_reservation_owner(catalog_id: str) -> str:
138
+ return f"{EXTENSION_PACK_RESERVATION_PREFIX}{catalog_id}"
139
+
140
+
141
+ def is_extension_pack_reservation_owner(owner: object) -> bool:
142
+ return isinstance(owner, str) and owner.startswith(EXTENSION_PACK_RESERVATION_PREFIX) and len(owner) > len(
143
+ EXTENSION_PACK_RESERVATION_PREFIX
144
+ )
145
+
146
+
147
+ def load_document_manifest(kind: str, *, include_untrusted: bool = False) -> list[dict[str, Any]]:
148
+ entries: list[dict[str, Any]] = []
149
+ for provider in _DOCUMENT_CATALOG_PROVIDERS:
150
+ if not include_untrusted and not provider["trusted_by_default"]:
151
+ continue
152
+ for raw_entry in provider["load_manifest"](kind):
153
+ entry = dict(raw_entry)
154
+ entry.setdefault("catalog_id", provider["catalog_id"])
155
+ entry.setdefault("trusted", provider["trusted_by_default"])
156
+ entries.append(entry)
157
+ return entries
158
+
159
+
160
+ def read_manifest_document_text(kind: str, manifest_entry: dict[str, Any]) -> str:
161
+ catalog_id = str(manifest_entry.get("catalog_id", "ssot-origin"))
162
+ for provider in _DOCUMENT_CATALOG_PROVIDERS:
163
+ if provider["catalog_id"] == catalog_id:
164
+ return provider["read_text"](kind, str(manifest_entry["filename"]))
165
+ raise ValueError(f"Unknown document catalog provider: {catalog_id}")
166
+
167
+
168
+ def read_manifest_document_bytes(kind: str, manifest_entry: dict[str, Any]) -> bytes:
169
+ catalog_id = str(manifest_entry.get("catalog_id", "ssot-origin"))
170
+ for provider in _DOCUMENT_CATALOG_PROVIDERS:
171
+ if provider["catalog_id"] == catalog_id:
172
+ return provider["read_bytes"](kind, str(manifest_entry["filename"]))
173
+ raise ValueError(f"Unknown document catalog provider: {catalog_id}")
119
174
 
120
175
 
121
176
  def read_packaged_document_text(kind: str, filename: str) -> str:
@@ -6,6 +6,7 @@ from typing import TypedDict
6
6
  class EvidenceRow(TypedDict, total=False):
7
7
  id: str
8
8
  title: str
9
+ body: str
9
10
  status: str
10
11
  kind: str
11
12
  tier: str
@@ -7,6 +7,7 @@ class FeatureRow(TypedDict, total=False):
7
7
  id: str
8
8
  title: str
9
9
  description: str
10
+ body: str
10
11
  implementation_status: str
11
12
  lifecycle: dict[str, object]
12
13
  plan: dict[str, object]
@@ -6,6 +6,7 @@ from typing import TypedDict
6
6
  class IssueRow(TypedDict, total=False):
7
7
  id: str
8
8
  title: str
9
+ body: str
9
10
  status: str
10
11
  severity: str
11
12
  description: str
@@ -7,6 +7,7 @@ class ProfileRow(TypedDict, total=False):
7
7
  id: str
8
8
  title: str
9
9
  description: str
10
+ body: str
10
11
  status: str
11
12
  kind: str
12
13
  feature_ids: list[str]