ssot-core 0.2.18.dev9__tar.gz → 0.2.19.dev2__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 (141) hide show
  1. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/PKG-INFO +6 -6
  2. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/README.md +2 -2
  3. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/pyproject.toml +4 -4
  4. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_core.egg-info/PKG-INFO +6 -6
  5. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_core.egg-info/SOURCES.txt +19 -1
  6. ssot_core-0.2.19.dev2/src/ssot_core.egg-info/requires.txt +7 -0
  7. ssot_core-0.2.19.dev2/src/ssot_registry/acl/__init__.py +5 -0
  8. ssot_core-0.2.19.dev2/src/ssot_registry/acl/policy.py +92 -0
  9. ssot_core-0.2.19.dev2/src/ssot_registry/acl/wrapper.py +36 -0
  10. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/__init__.py +8 -0
  11. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/entity_ops.py +92 -1
  12. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/origin.py +8 -0
  13. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/upgrade.py +39 -5
  14. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/validate.py +2 -0
  15. ssot_core-0.2.19.dev2/src/ssot_registry/control/__init__.py +6 -0
  16. ssot_core-0.2.19.dev2/src/ssot_registry/control/events.py +17 -0
  17. ssot_core-0.2.19.dev2/src/ssot_registry/control/models.py +42 -0
  18. ssot_core-0.2.19.dev2/src/ssot_registry/control/paths.py +66 -0
  19. ssot_core-0.2.19.dev2/src/ssot_registry/control/scaffold.py +165 -0
  20. ssot_core-0.2.19.dev2/src/ssot_registry/control/service.py +605 -0
  21. ssot_core-0.2.19.dev2/src/ssot_registry/control/sqlite_store.py +653 -0
  22. ssot_core-0.2.19.dev2/src/ssot_registry/control/sse.py +52 -0
  23. ssot_core-0.2.19.dev2/src/ssot_registry/guards/completion.py +85 -0
  24. ssot_core-0.2.19.dev2/src/ssot_registry/maturation/__init__.py +21 -0
  25. ssot_core-0.2.19.dev2/src/ssot_registry/maturation/selector.py +320 -0
  26. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/enums.py +2 -1
  27. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/feature.py +1 -0
  28. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/schema_version.py +1 -1
  29. ssot_core-0.2.19.dev2/src/ssot_registry/templates/registry.full.json +1 -0
  30. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/templates/registry.minimal.json +1 -1
  31. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/__init__.py +2 -0
  32. ssot_core-0.2.19.dev2/src/ssot_registry/validators/feature_parent_links.py +49 -0
  33. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/structure.py +3 -0
  34. ssot_core-0.2.19.dev2/src/ssot_registry/watch/__init__.py +5 -0
  35. ssot_core-0.2.19.dev2/src/ssot_registry/watch/git_status.py +29 -0
  36. ssot_core-0.2.19.dev2/src/ssot_registry/watch/observer.py +95 -0
  37. ssot_core-0.2.18.dev9/src/ssot_core.egg-info/requires.txt +0 -7
  38. ssot_core-0.2.18.dev9/src/ssot_registry/templates/registry.full.json +0 -1
  39. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/setup.cfg +0 -0
  40. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_core.egg-info/dependency_links.txt +0 -0
  41. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_core.egg-info/top_level.txt +0 -0
  42. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/__init__.py +0 -0
  43. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/__main__.py +0 -0
  44. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/boundary.py +0 -0
  45. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/claims.py +0 -0
  46. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/config.py +0 -0
  47. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/documents.py +0 -0
  48. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/evidence.py +0 -0
  49. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/graph.py +0 -0
  50. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/init.py +0 -0
  51. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/lifecycle.py +0 -0
  52. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/load.py +0 -0
  53. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/local_assurance.py +0 -0
  54. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/packs.py +0 -0
  55. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/plan.py +0 -0
  56. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/profile_eval.py +0 -0
  57. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/profile_resolution.py +0 -0
  58. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/registry.py +0 -0
  59. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/release.py +0 -0
  60. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/save.py +0 -0
  61. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/status_sync.py +0 -0
  62. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/api/test_execution.py +0 -0
  63. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/__init__.py +0 -0
  64. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/adr_cmd.py +0 -0
  65. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/boundary_cmd.py +0 -0
  66. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/claim_cmd.py +0 -0
  67. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/common.py +0 -0
  68. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/evidence_cmd.py +0 -0
  69. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/feature_cmd.py +0 -0
  70. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/graph_cmd.py +0 -0
  71. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/init_cmd.py +0 -0
  72. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/issue_cmd.py +0 -0
  73. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/main.py +0 -0
  74. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/profile_cmd.py +0 -0
  75. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/registry_cmd.py +0 -0
  76. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/release_cmd.py +0 -0
  77. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/risk_cmd.py +0 -0
  78. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/spec_cmd.py +0 -0
  79. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/test_cmd.py +0 -0
  80. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/upgrade_cmd.py +0 -0
  81. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/cli/validate_cmd.py +0 -0
  82. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/graph/__init__.py +0 -0
  83. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/graph/export_dot.py +0 -0
  84. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/graph/export_json.py +0 -0
  85. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/__init__.py +0 -0
  86. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/certification.py +0 -0
  87. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/claim_closure.py +0 -0
  88. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/claim_tier_gates.py +0 -0
  89. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/document_lifecycle.py +0 -0
  90. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/document_supersession.py +0 -0
  91. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/feature_claims.py +0 -0
  92. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/feature_requirements.py +0 -0
  93. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/lifecycle.py +0 -0
  94. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/profile_requirements.py +0 -0
  95. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/promotion.py +0 -0
  96. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/guards/publication.py +0 -0
  97. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/__init__.py +0 -0
  98. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/boundary.py +0 -0
  99. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/claim.py +0 -0
  100. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/document.py +0 -0
  101. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/evidence.py +0 -0
  102. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/ids.py +0 -0
  103. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/issue.py +0 -0
  104. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/profile.py +0 -0
  105. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/registry.py +0 -0
  106. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/release.py +0 -0
  107. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/risk.py +0 -0
  108. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/model/test.py +0 -0
  109. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/reports/__init__.py +0 -0
  110. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/reports/certification_report.py +0 -0
  111. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/reports/summary.py +0 -0
  112. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/reports/validation_report.py +0 -0
  113. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/snapshots/__init__.py +0 -0
  114. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/snapshots/boundary_snapshot.py +0 -0
  115. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/snapshots/hashing.py +0 -0
  116. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/snapshots/published_snapshot.py +0 -0
  117. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/snapshots/release_snapshot.py +0 -0
  118. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/templates/__init__.py +0 -0
  119. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/__init__.py +0 -0
  120. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/document_io.py +0 -0
  121. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/errors.py +0 -0
  122. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/formatting.py +0 -0
  123. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/fs.py +0 -0
  124. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/jcs.py +0 -0
  125. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/jsonio.py +0 -0
  126. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/registry_lock.py +0 -0
  127. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/util/time.py +0 -0
  128. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/bidirectional.py +0 -0
  129. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/bounds.py +0 -0
  130. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/claim_lineage.py +0 -0
  131. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/coverage.py +0 -0
  132. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/documents.py +0 -0
  133. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/filesystem.py +0 -0
  134. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/identity.py +0 -0
  135. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/lifecycle.py +0 -0
  136. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/origin.py +0 -0
  137. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/promotion.py +0 -0
  138. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/references.py +0 -0
  139. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/reservations.py +0 -0
  140. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/src/ssot_registry/validators/tiers.py +0 -0
  141. {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev2}/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.18.dev9
3
+ Version: 0.2.19.dev2
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
@@ -36,9 +36,9 @@ Classifier: Topic :: Utilities
36
36
  Requires-Python: <3.15,>=3.10
37
37
  Description-Content-Type: text/markdown
38
38
  Requires-Dist: orjson<4.0,>=3.10
39
- Requires-Dist: ssot-contracts==0.2.18.dev9
40
- Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.17
41
- Requires-Dist: ssot-views==0.2.18.dev9
39
+ Requires-Dist: ssot-contracts==0.2.19.dev2
40
+ Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.20.dev2
41
+ Requires-Dist: ssot-views==0.2.19.dev2
42
42
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
43
43
 
44
44
  <div align="center">
@@ -52,8 +52,8 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
52
52
  <a href="https://pepy.tech/project/ssot-core"><img src="https://static.pepy.tech/badge/ssot-core" alt="Downloads" /></a>
53
53
  <a href="https://hits.sh/github.com/groupsum/ssot-registry/"><img src="https://hits.sh/github.com/groupsum/ssot-registry.svg?style=flat-square" alt="Hits" /></a>
54
54
  <!-- ssot-schema-badges:start -->
55
- <img src="https://img.shields.io/badge/schema_version-0.5.0-blue" alt="schema_version 0.5.0" />
56
- <img src="https://img.shields.io/badge/migration%20coverage-12%2F12-brightgreen" alt="Migration coverage 12/12" />
55
+ <img src="https://img.shields.io/badge/schema_version-0.7.0-blue" alt="schema_version 0.7.0" />
56
+ <img src="https://img.shields.io/badge/migration%20coverage-14%2F14-brightgreen" alt="Migration coverage 14/14" />
57
57
  <!-- ssot-schema-badges:end -->
58
58
  </div>
59
59
 
@@ -9,8 +9,8 @@
9
9
  <a href="https://pepy.tech/project/ssot-core"><img src="https://static.pepy.tech/badge/ssot-core" alt="Downloads" /></a>
10
10
  <a href="https://hits.sh/github.com/groupsum/ssot-registry/"><img src="https://hits.sh/github.com/groupsum/ssot-registry.svg?style=flat-square" alt="Hits" /></a>
11
11
  <!-- ssot-schema-badges:start -->
12
- <img src="https://img.shields.io/badge/schema_version-0.5.0-blue" alt="schema_version 0.5.0" />
13
- <img src="https://img.shields.io/badge/migration%20coverage-12%2F12-brightgreen" alt="Migration coverage 12/12" />
12
+ <img src="https://img.shields.io/badge/schema_version-0.7.0-blue" alt="schema_version 0.7.0" />
13
+ <img src="https://img.shields.io/badge/migration%20coverage-14%2F14-brightgreen" alt="Migration coverage 14/14" />
14
14
  <!-- ssot-schema-badges:end -->
15
15
  </div>
16
16
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ssot-core"
7
- version = "0.2.18.dev9"
7
+ version = "0.2.19.dev2"
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.15"
@@ -12,9 +12,9 @@ license = "Apache-2.0"
12
12
  authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
13
13
  dependencies = [
14
14
  "orjson>=3.10,<4.0",
15
- "ssot-contracts==0.2.18.dev9",
16
- "ssot-pack-contracts>=0.2.17,<0.3.0",
17
- "ssot-views==0.2.18.dev9",
15
+ "ssot-contracts==0.2.19.dev2",
16
+ "ssot-pack-contracts>=0.2.20.dev2,<0.3.0",
17
+ "ssot-views==0.2.19.dev2",
18
18
  "tomli>=2.0.1; python_version < '3.11'",
19
19
  ]
20
20
  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.18.dev9
3
+ Version: 0.2.19.dev2
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
@@ -36,9 +36,9 @@ Classifier: Topic :: Utilities
36
36
  Requires-Python: <3.15,>=3.10
37
37
  Description-Content-Type: text/markdown
38
38
  Requires-Dist: orjson<4.0,>=3.10
39
- Requires-Dist: ssot-contracts==0.2.18.dev9
40
- Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.17
41
- Requires-Dist: ssot-views==0.2.18.dev9
39
+ Requires-Dist: ssot-contracts==0.2.19.dev2
40
+ Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.20.dev2
41
+ Requires-Dist: ssot-views==0.2.19.dev2
42
42
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
43
43
 
44
44
  <div align="center">
@@ -52,8 +52,8 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
52
52
  <a href="https://pepy.tech/project/ssot-core"><img src="https://static.pepy.tech/badge/ssot-core" alt="Downloads" /></a>
53
53
  <a href="https://hits.sh/github.com/groupsum/ssot-registry/"><img src="https://hits.sh/github.com/groupsum/ssot-registry.svg?style=flat-square" alt="Hits" /></a>
54
54
  <!-- ssot-schema-badges:start -->
55
- <img src="https://img.shields.io/badge/schema_version-0.5.0-blue" alt="schema_version 0.5.0" />
56
- <img src="https://img.shields.io/badge/migration%20coverage-12%2F12-brightgreen" alt="Migration coverage 12/12" />
55
+ <img src="https://img.shields.io/badge/schema_version-0.7.0-blue" alt="schema_version 0.7.0" />
56
+ <img src="https://img.shields.io/badge/migration%20coverage-14%2F14-brightgreen" alt="Migration coverage 14/14" />
57
57
  <!-- ssot-schema-badges:end -->
58
58
  </div>
59
59
 
@@ -8,6 +8,9 @@ src/ssot_core.egg-info/top_level.txt
8
8
  src/ssot_registry/__init__.py
9
9
  src/ssot_registry/__main__.py
10
10
  src/ssot_registry/version.py
11
+ src/ssot_registry/acl/__init__.py
12
+ src/ssot_registry/acl/policy.py
13
+ src/ssot_registry/acl/wrapper.py
11
14
  src/ssot_registry/api/__init__.py
12
15
  src/ssot_registry/api/boundary.py
13
16
  src/ssot_registry/api/claims.py
@@ -51,6 +54,14 @@ src/ssot_registry/cli/spec_cmd.py
51
54
  src/ssot_registry/cli/test_cmd.py
52
55
  src/ssot_registry/cli/upgrade_cmd.py
53
56
  src/ssot_registry/cli/validate_cmd.py
57
+ src/ssot_registry/control/__init__.py
58
+ src/ssot_registry/control/events.py
59
+ src/ssot_registry/control/models.py
60
+ src/ssot_registry/control/paths.py
61
+ src/ssot_registry/control/scaffold.py
62
+ src/ssot_registry/control/service.py
63
+ src/ssot_registry/control/sqlite_store.py
64
+ src/ssot_registry/control/sse.py
54
65
  src/ssot_registry/graph/__init__.py
55
66
  src/ssot_registry/graph/export_dot.py
56
67
  src/ssot_registry/graph/export_json.py
@@ -58,6 +69,7 @@ src/ssot_registry/guards/__init__.py
58
69
  src/ssot_registry/guards/certification.py
59
70
  src/ssot_registry/guards/claim_closure.py
60
71
  src/ssot_registry/guards/claim_tier_gates.py
72
+ src/ssot_registry/guards/completion.py
61
73
  src/ssot_registry/guards/document_lifecycle.py
62
74
  src/ssot_registry/guards/document_supersession.py
63
75
  src/ssot_registry/guards/feature_claims.py
@@ -66,6 +78,8 @@ src/ssot_registry/guards/lifecycle.py
66
78
  src/ssot_registry/guards/profile_requirements.py
67
79
  src/ssot_registry/guards/promotion.py
68
80
  src/ssot_registry/guards/publication.py
81
+ src/ssot_registry/maturation/__init__.py
82
+ src/ssot_registry/maturation/selector.py
69
83
  src/ssot_registry/model/__init__.py
70
84
  src/ssot_registry/model/boundary.py
71
85
  src/ssot_registry/model/claim.py
@@ -108,6 +122,7 @@ src/ssot_registry/validators/bounds.py
108
122
  src/ssot_registry/validators/claim_lineage.py
109
123
  src/ssot_registry/validators/coverage.py
110
124
  src/ssot_registry/validators/documents.py
125
+ src/ssot_registry/validators/feature_parent_links.py
111
126
  src/ssot_registry/validators/filesystem.py
112
127
  src/ssot_registry/validators/identity.py
113
128
  src/ssot_registry/validators/lifecycle.py
@@ -116,4 +131,7 @@ src/ssot_registry/validators/promotion.py
116
131
  src/ssot_registry/validators/references.py
117
132
  src/ssot_registry/validators/reservations.py
118
133
  src/ssot_registry/validators/structure.py
119
- src/ssot_registry/validators/tiers.py
134
+ src/ssot_registry/validators/tiers.py
135
+ src/ssot_registry/watch/__init__.py
136
+ src/ssot_registry/watch/git_status.py
137
+ src/ssot_registry/watch/observer.py
@@ -0,0 +1,7 @@
1
+ orjson<4.0,>=3.10
2
+ ssot-contracts==0.2.19.dev2
3
+ ssot-pack-contracts<0.3.0,>=0.2.20.dev2
4
+ ssot-views==0.2.19.dev2
5
+
6
+ [:python_version < "3.11"]
7
+ tomli>=2.0.1
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from .policy import AclCommand, AclPolicy, LinuxAclAdapter, WindowsAclAdapter
4
+
5
+ __all__ = ["AclCommand", "AclPolicy", "LinuxAclAdapter", "WindowsAclAdapter"]
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Protocol
8
+
9
+ from ssot_registry.control.paths import ensure_allowed_path, repo_relative_path
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class AclCommand:
14
+ argv: tuple[str, ...]
15
+ path: str
16
+ action: str
17
+ worker_user: str
18
+
19
+
20
+ class AclAdapter(Protocol):
21
+ def grant_commands(self, repo_root: Path, worker_user: str, paths: list[str]) -> list[AclCommand]: ...
22
+
23
+ def revoke_commands(self, repo_root: Path, worker_user: str, paths: list[str]) -> list[AclCommand]: ...
24
+
25
+
26
+ class AclPolicy:
27
+ def __init__(self, repo_root: str | Path, adapter: AclAdapter | None = None) -> None:
28
+ self.repo_root = Path(repo_root)
29
+ self.adapter = adapter or default_acl_adapter()
30
+
31
+ def normalize_roots(self, paths: list[str | Path]) -> list[str]:
32
+ return [ensure_allowed_path(repo_relative_path(self.repo_root, path)) for path in paths]
33
+
34
+ def grant_commands(self, worker_user: str, paths: list[str | Path]) -> list[AclCommand]:
35
+ return self.adapter.grant_commands(self.repo_root, worker_user, self.normalize_roots(paths))
36
+
37
+ def revoke_commands(self, worker_user: str, paths: list[str | Path]) -> list[AclCommand]:
38
+ return self.adapter.revoke_commands(self.repo_root, worker_user, self.normalize_roots(paths))
39
+
40
+ def apply(self, commands: list[AclCommand]) -> None:
41
+ for command in commands:
42
+ subprocess.run(command.argv, check=True)
43
+
44
+ def grant(self, worker_user: str, paths: list[str | Path]) -> list[AclCommand]:
45
+ commands = self.grant_commands(worker_user, paths)
46
+ self.apply(commands)
47
+ return commands
48
+
49
+ def revoke(self, worker_user: str, paths: list[str | Path]) -> list[AclCommand]:
50
+ commands = self.revoke_commands(worker_user, paths)
51
+ self.apply(commands)
52
+ return commands
53
+
54
+
55
+ class LinuxAclAdapter:
56
+ def grant_commands(self, repo_root: Path, worker_user: str, paths: list[str]) -> list[AclCommand]:
57
+ commands: list[AclCommand] = []
58
+ for path in paths:
59
+ absolute = str(repo_root / path)
60
+ commands.append(AclCommand(("setfacl", "-R", "-m", f"u:{worker_user}:rwX", absolute), path, "grant", worker_user))
61
+ commands.append(AclCommand(("setfacl", "-R", "-d", "-m", f"u:{worker_user}:rwX", absolute), path, "grant-default", worker_user))
62
+ return commands
63
+
64
+ def revoke_commands(self, repo_root: Path, worker_user: str, paths: list[str]) -> list[AclCommand]:
65
+ commands: list[AclCommand] = []
66
+ for path in paths:
67
+ absolute = str(repo_root / path)
68
+ commands.append(AclCommand(("setfacl", "-R", "-x", f"u:{worker_user}", absolute), path, "revoke", worker_user))
69
+ commands.append(AclCommand(("setfacl", "-R", "-d", "-x", f"u:{worker_user}", absolute), path, "revoke-default", worker_user))
70
+ return commands
71
+
72
+
73
+ class WindowsAclAdapter:
74
+ def grant_commands(self, repo_root: Path, worker_user: str, paths: list[str]) -> list[AclCommand]:
75
+ commands: list[AclCommand] = []
76
+ for path in paths:
77
+ absolute = str(repo_root / path)
78
+ commands.append(AclCommand(("icacls", absolute, "/grant", f"{worker_user}:(OI)(CI)(M)"), path, "grant", worker_user))
79
+ return commands
80
+
81
+ def revoke_commands(self, repo_root: Path, worker_user: str, paths: list[str]) -> list[AclCommand]:
82
+ commands: list[AclCommand] = []
83
+ for path in paths:
84
+ absolute = str(repo_root / path)
85
+ commands.append(AclCommand(("icacls", absolute, "/remove", worker_user), path, "revoke", worker_user))
86
+ return commands
87
+
88
+
89
+ def default_acl_adapter() -> AclAdapter:
90
+ if platform.system().lower().startswith("win"):
91
+ return WindowsAclAdapter()
92
+ return LinuxAclAdapter()
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from .policy import AclPolicy
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ parser = argparse.ArgumentParser(description="Narrow ACL helper for SSOT path lease grants and revocations.")
11
+ parser.add_argument("action", choices=["grant", "revoke"])
12
+ parser.add_argument("repo_root")
13
+ parser.add_argument("worker_user")
14
+ parser.add_argument("paths", nargs="+")
15
+ parser.add_argument("--dry-run", action="store_true")
16
+ return parser
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ args = build_parser().parse_args(argv)
21
+ policy = AclPolicy(Path(args.repo_root))
22
+ commands = (
23
+ policy.grant_commands(args.worker_user, args.paths)
24
+ if args.action == "grant"
25
+ else policy.revoke_commands(args.worker_user, args.paths)
26
+ )
27
+ if args.dry_run:
28
+ for command in commands:
29
+ print(" ".join(command.argv))
30
+ return 0
31
+ policy.apply(commands)
32
+ return 0
33
+
34
+
35
+ if __name__ == "__main__":
36
+ raise SystemExit(main())
@@ -20,6 +20,7 @@ from .documents import (
20
20
  from .entity_ops import (
21
21
  add_boundary_features,
22
22
  add_boundary_profiles,
23
+ add_feature_children,
23
24
  add_release_boundaries,
24
25
  add_release_claims,
25
26
  add_release_evidence,
@@ -27,14 +28,17 @@ from .entity_ops import (
27
28
  delete_entity,
28
29
  get_entity,
29
30
  link_entities,
31
+ list_feature_children,
30
32
  list_entities,
31
33
  remove_boundary_features,
32
34
  remove_boundary_profiles,
35
+ remove_feature_children,
33
36
  remove_release_boundaries,
34
37
  remove_release_claims,
35
38
  remove_release_evidence,
36
39
  set_claim_status,
37
40
  set_claim_tier,
41
+ set_feature_parents,
38
42
  set_issue_status,
39
43
  set_risk_status,
40
44
  unlink_entities,
@@ -102,6 +106,10 @@ __all__ = [
102
106
  "add_boundary_profiles",
103
107
  "remove_boundary_features",
104
108
  "remove_boundary_profiles",
109
+ "set_feature_parents",
110
+ "add_feature_children",
111
+ "remove_feature_children",
112
+ "list_feature_children",
105
113
  "add_release_boundaries",
106
114
  "remove_release_boundaries",
107
115
  "add_release_claims",
@@ -40,7 +40,7 @@ RECIPROCAL_FIELDS = {
40
40
  }
41
41
 
42
42
  LINKABLE_FIELDS = {
43
- "features": {"spec_ids", "claim_ids", "test_ids", "requires"},
43
+ "features": {"spec_ids", "claim_ids", "test_ids", "requires", "parent_feature_ids"},
44
44
  "profiles": {"feature_ids", "profile_ids"},
45
45
  "tests": {"feature_ids", "claim_ids", "evidence_ids"},
46
46
  "claims": {"feature_ids", "test_ids", "evidence_ids", "depends_on_claim_ids"},
@@ -76,6 +76,19 @@ def _dedupe_preserve(values: list[str]) -> list[str]:
76
76
  return ordered
77
77
 
78
78
 
79
+ def _dedupe_sorted(values: list[str]) -> list[str]:
80
+ return sorted(dict.fromkeys(values))
81
+
82
+
83
+ def _normalize_feature_parent_ids(row: dict[str, Any]) -> None:
84
+ values = row.get("parent_feature_ids")
85
+ if values is None:
86
+ row["parent_feature_ids"] = []
87
+ return
88
+ if isinstance(values, list):
89
+ row["parent_feature_ids"] = _dedupe_sorted(values)
90
+
91
+
79
92
  def _repo_kind(registry: dict[str, Any]) -> str:
80
93
  repo = registry.get("repo")
81
94
  if isinstance(repo, dict):
@@ -221,6 +234,9 @@ def create_entity(path: str | Path, section: str, row: dict[str, Any]) -> dict[s
221
234
  _ensure_assurance_origin(registry, section, candidate)
222
235
  if section == "claims":
223
236
  candidate.setdefault("depends_on_claim_ids", [])
237
+ if section == "features":
238
+ candidate.setdefault("parent_feature_ids", [])
239
+ _normalize_feature_parent_ids(candidate)
224
240
  _validate_assurance_origin_mutation(registry, section, candidate)
225
241
  for field_name in LINKABLE_FIELDS[section]:
226
242
  if field_name in candidate and isinstance(candidate[field_name], list):
@@ -278,6 +294,8 @@ def update_entity(path: str | Path, section: str, entity_id: str, changes: dict[
278
294
  continue
279
295
  row[field_name] = deepcopy(value)
280
296
  _validate_assurance_origin_mutation(registry, section, row, current=current_row)
297
+ if section == "features":
298
+ _normalize_feature_parent_ids(row)
281
299
  if isinstance(next_id, str) and next_id != entity_id:
282
300
  _rewrite_references_for_renamed_id(registry, target_section=section, old_id=entity_id, new_id=next_id)
283
301
  action = f"renaming {SECTION_LABELS[section]} {entity_id} to {next_id}"
@@ -359,6 +377,8 @@ def link_entities(path: str | Path, section: str, entity_id: str, links: dict[st
359
377
  values.append(target_id)
360
378
  _apply_reciprocal_add(registry, section, field_name, entity_id, target_id)
361
379
  row[field_name] = _dedupe_preserve(values)
380
+ if section == "features":
381
+ _normalize_feature_parent_ids(row)
362
382
  mutation = _validate_and_save(registry_path, repo_root, registry, f"linking {SECTION_LABELS[section]} {entity_id}")
363
383
  return {
364
384
  "passed": True,
@@ -381,6 +401,8 @@ def unlink_entities(path: str | Path, section: str, entity_id: str, links: dict[
381
401
  values.remove(target_id)
382
402
  _apply_reciprocal_remove(registry, section, field_name, entity_id, target_id)
383
403
  row[field_name] = _dedupe_preserve(values)
404
+ if section == "features":
405
+ _normalize_feature_parent_ids(row)
384
406
  mutation = _validate_and_save(registry_path, repo_root, registry, f"unlinking {SECTION_LABELS[section]} {entity_id}")
385
407
  return {
386
408
  "passed": True,
@@ -395,6 +417,75 @@ def set_claim_status(path: str | Path, claim_id: str, status: str) -> dict[str,
395
417
  return update_entity(path, "claims", claim_id, {"status": status})
396
418
 
397
419
 
420
+ def set_feature_parents(path: str | Path, ids: list[str], parent_ids: list[str], mode: str) -> dict[str, Any]:
421
+ if mode not in {"add", "set", "remove", "clear"}:
422
+ raise ValueError("mode must be one of add, set, remove, or clear")
423
+ registry_path, repo_root, registry = load_registry(path)
424
+ feature_lookup = _row_lookup(registry, "features")
425
+ target_ids = _dedupe_preserve(ids)
426
+ requested_parent_ids = _dedupe_preserve(parent_ids)
427
+ if mode == "clear":
428
+ requested_parent_ids = []
429
+ elif not requested_parent_ids:
430
+ raise ValueError("At least one parent feature id is required")
431
+
432
+ missing_targets = sorted(entity_id for entity_id in target_ids if entity_id not in feature_lookup)
433
+ missing_parents = sorted(parent_id for parent_id in requested_parent_ids if parent_id not in feature_lookup)
434
+ if missing_targets or missing_parents:
435
+ messages: list[str] = []
436
+ if missing_targets:
437
+ messages.append(f"Unknown feature ids: {', '.join(missing_targets)}")
438
+ if missing_parents:
439
+ messages.append(f"Unknown parent feature ids: {', '.join(missing_parents)}")
440
+ raise ValueError("; ".join(messages))
441
+
442
+ for entity_id in target_ids:
443
+ row = feature_lookup[entity_id]
444
+ current = _ensure_list_field(row, "parent_feature_ids")
445
+ if mode == "add":
446
+ row["parent_feature_ids"] = _dedupe_sorted([*current, *requested_parent_ids])
447
+ elif mode == "set":
448
+ row["parent_feature_ids"] = _dedupe_sorted(requested_parent_ids)
449
+ elif mode == "remove":
450
+ removals = set(requested_parent_ids)
451
+ row["parent_feature_ids"] = _dedupe_sorted([value for value in current if value not in removals])
452
+ else:
453
+ row["parent_feature_ids"] = []
454
+
455
+ mutation = _validate_and_save(registry_path, repo_root, registry, f"{mode} feature parent links")
456
+ return {
457
+ "passed": True,
458
+ "registry_path": registry_path.as_posix(),
459
+ "section": "features",
460
+ "mode": mode,
461
+ "ids": target_ids,
462
+ "parent_ids": requested_parent_ids,
463
+ "entities": [deepcopy(feature_lookup[entity_id]) for entity_id in target_ids],
464
+ **mutation,
465
+ }
466
+
467
+
468
+ def add_feature_children(path: str | Path, parent_id: str, child_ids: list[str]) -> dict[str, Any]:
469
+ return set_feature_parents(path, child_ids, [parent_id], "add")
470
+
471
+
472
+ def remove_feature_children(path: str | Path, parent_id: str, child_ids: list[str]) -> dict[str, Any]:
473
+ return set_feature_parents(path, child_ids, [parent_id], "remove")
474
+
475
+
476
+ def list_feature_children(path: str | Path, parent_id: str) -> list[dict[str, Any]]:
477
+ _registry_path, _repo_root, registry = load_registry(path)
478
+ feature_lookup = _row_lookup(registry, "features")
479
+ if parent_id not in feature_lookup:
480
+ raise ValueError(f"Unknown feature id: {parent_id}")
481
+ rows = [
482
+ deepcopy(row)
483
+ for row in registry.get("features", [])
484
+ if isinstance(row, dict) and parent_id in row.get("parent_feature_ids", [])
485
+ ]
486
+ return sorted(rows, key=lambda row: row["id"])
487
+
488
+
398
489
  def set_claim_tier(path: str | Path, claim_id: str, tier: str) -> dict[str, Any]:
399
490
  registry_path, repo_root, registry = load_registry(path)
400
491
  row = _entity_row(registry, "claims", claim_id)
@@ -20,6 +20,12 @@ def _origin_rows(registry: dict[str, Any], section: str) -> dict[str, dict[str,
20
20
  }
21
21
 
22
22
 
23
+ def _normalize_feature_rows(registry: dict[str, Any]) -> None:
24
+ for row in registry.get("features", []):
25
+ if isinstance(row, dict):
26
+ row.setdefault("parent_feature_ids", [])
27
+
28
+
23
29
  def _load_source_registry(source: str | Path | dict[str, Any]) -> dict[str, Any]:
24
30
  if isinstance(source, dict):
25
31
  return deepcopy(source)
@@ -36,6 +42,8 @@ def sync_origin_assurance_rows(
36
42
  ) -> dict[str, Any]:
37
43
  registry_path, repo_root, registry = load_registry(path)
38
44
  source_registry = _load_source_registry(source)
45
+ _normalize_feature_rows(registry)
46
+ _normalize_feature_rows(source_registry)
39
47
  selected_sections = tuple(sections or ASSURANCE_ENTITY_SECTIONS)
40
48
  unsupported = sorted(set(selected_sections) - set(ASSURANCE_ENTITY_SECTIONS))
41
49
  if unsupported:
@@ -40,6 +40,7 @@ SCHEMA_V0_2_0 = "0.2.0"
40
40
  SCHEMA_V0_3_0 = "0.3.0"
41
41
  SCHEMA_V0_4_0 = "0.4.0"
42
42
  SCHEMA_V0_5_0 = "0.5.0"
43
+ SCHEMA_V0_6_0 = "0.6.0"
43
44
  MIGRATION_RELEASE_WINDOWS = {
44
45
  (3, 4): "0.1.x->0.2.1",
45
46
  (4, 5): "0.2.1->0.2.2",
@@ -53,7 +54,8 @@ MIGRATION_RELEASE_WINDOWS = {
53
54
  (SCHEMA_V0_2_0, SCHEMA_V0_3_0): "0.2.10->0.3.0",
54
55
  (SCHEMA_V0_3_0, SCHEMA_V0_4_0): "0.2.10->0.4.0",
55
56
  (SCHEMA_V0_4_0, SCHEMA_V0_5_0): "0.4.0->0.5.0",
56
- (SCHEMA_V0_5_0, SCHEMA_VERSION): "0.5.0->0.6.0",
57
+ (SCHEMA_V0_5_0, SCHEMA_V0_6_0): "0.5.0->0.6.0",
58
+ (SCHEMA_V0_6_0, SCHEMA_VERSION): "0.6.0->0.7.0",
57
59
  }
58
60
 
59
61
  MIGRATION_PATHS = (
@@ -69,7 +71,8 @@ MIGRATION_PATHS = (
69
71
  (SCHEMA_V0_2_0, SCHEMA_V0_3_0, "migrate_v0_2_0_to_v0_3_0"),
70
72
  (SCHEMA_V0_3_0, SCHEMA_V0_4_0, "migrate_v0_3_0_to_v0_4_0"),
71
73
  (SCHEMA_V0_4_0, SCHEMA_V0_5_0, "migrate_v0_4_0_to_v0_5_0"),
72
- (SCHEMA_V0_5_0, SCHEMA_VERSION, "migrate_v0_5_0_to_v0_6_0"),
74
+ (SCHEMA_V0_5_0, SCHEMA_V0_6_0, "migrate_v0_5_0_to_v0_6_0"),
75
+ (SCHEMA_V0_6_0, SCHEMA_VERSION, "migrate_v0_6_0_to_v0_7_0"),
73
76
  )
74
77
 
75
78
 
@@ -594,13 +597,34 @@ def migrate_v0_5_0_to_v0_6_0(
594
597
  ) -> dict[str, Any]:
595
598
  _ = repo_root, previous_version, target_version
596
599
  migrated = deepcopy(registry)
597
- migrated["schema_version"] = SCHEMA_VERSION
600
+ migrated["schema_version"] = SCHEMA_V0_6_0
598
601
  for claim in migrated.get("claims", []):
599
602
  if isinstance(claim, dict):
600
603
  claim.setdefault("depends_on_claim_ids", [])
601
604
  return migrated
602
605
 
603
606
 
607
+ def migrate_v0_6_0_to_v0_7_0(
608
+ registry: dict[str, Any],
609
+ repo_root: Path,
610
+ *,
611
+ previous_version: str,
612
+ target_version: str,
613
+ ) -> dict[str, Any]:
614
+ _ = repo_root, previous_version, target_version
615
+ migrated = deepcopy(registry)
616
+ migrated["schema_version"] = SCHEMA_VERSION
617
+ for feature in migrated.get("features", []):
618
+ if not isinstance(feature, dict):
619
+ continue
620
+ parent_feature_ids = feature.get("parent_feature_ids", [])
621
+ if not isinstance(parent_feature_ids, list) or not all(isinstance(value, str) for value in parent_feature_ids):
622
+ feature_id = feature.get("id", "<missing>")
623
+ raise ValidationError(f"features.{feature_id}.parent_feature_ids must be a list of strings")
624
+ feature["parent_feature_ids"] = sorted(dict.fromkeys(parent_feature_ids))
625
+ return migrated
626
+
627
+
604
628
  def target_version_from_registry(registry: dict[str, Any]) -> str:
605
629
  tooling = registry.get("tooling")
606
630
  if isinstance(tooling, dict):
@@ -767,11 +791,21 @@ def upgrade_registry(
767
791
  target_version=target_version,
768
792
  )
769
793
  schema_migrations.append("migrate_v0_5_0_to_v0_6_0")
770
- migrations.append(_migration_window_label(SCHEMA_V0_5_0, SCHEMA_VERSION))
794
+ migrations.append(_migration_window_label(SCHEMA_V0_5_0, SCHEMA_V0_6_0))
795
+ source_schema = SCHEMA_V0_6_0
796
+ if source_schema == SCHEMA_V0_6_0:
797
+ working = migrate_v0_6_0_to_v0_7_0(
798
+ working,
799
+ repo_root,
800
+ previous_version=source_tooling_version,
801
+ target_version=target_version,
802
+ )
803
+ schema_migrations.append("migrate_v0_6_0_to_v0_7_0")
804
+ migrations.append(_migration_window_label(SCHEMA_V0_6_0, SCHEMA_VERSION))
771
805
  source_schema = SCHEMA_VERSION
772
806
  elif source_schema != SCHEMA_VERSION:
773
807
  raise RegistryError(
774
- 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, 0.4.0, 0.5.0 or {SCHEMA_VERSION}"
808
+ 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, 0.4.0, 0.5.0, 0.6.0 or {SCHEMA_VERSION}"
775
809
  )
776
810
 
777
811
  normalized_current = _normalize_current_registry(working)
@@ -19,6 +19,7 @@ from ssot_registry.validators import (
19
19
  validate_out_of_bounds_disposition,
20
20
  validate_filesystem_paths,
21
21
  validate_assurance_origins,
22
+ validate_feature_parent_links,
22
23
  )
23
24
  from .load import load_registry
24
25
 
@@ -43,6 +44,7 @@ def validate_registry_document(
43
44
  validate_lifecycle_semantics(registry, index, failures, warnings)
44
45
  validate_out_of_bounds_disposition(registry, index, failures, warnings)
45
46
  validate_assurance_origins(registry, index, failures)
47
+ validate_feature_parent_links(index, failures)
46
48
  validate_filesystem_paths(registry, index, Path(repo_root), failures, warnings)
47
49
 
48
50
  return build_validation_report(registry, Path(registry_path).as_posix(), sorted(set(failures)), sorted(set(warnings)))
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from .service import ControlPlane
4
+ from .sqlite_store import ControlStore
5
+
6
+ __all__ = ["ControlPlane", "ControlStore"]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def format_sse_event(event: dict[str, Any]) -> str:
8
+ event_id = event.get("event_id")
9
+ kind = event.get("kind", "message")
10
+ data = json.dumps(event, sort_keys=True, separators=(",", ":"))
11
+ return f"id: {event_id}\nevent: {kind}\ndata: {data}\n\n"
12
+
13
+
14
+ def event_cursor(events: list[dict[str, Any]]) -> int:
15
+ if not events:
16
+ return 0
17
+ return max(int(event.get("event_id", 0)) for event in events)