ssot-core 0.2.14.dev1__tar.gz → 0.2.15.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 (114) hide show
  1. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/PKG-INFO +9 -7
  2. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/README.md +6 -4
  3. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/pyproject.toml +3 -3
  4. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_core.egg-info/PKG-INFO +9 -7
  5. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_core.egg-info/SOURCES.txt +1 -0
  6. ssot_core-0.2.15.dev2/src/ssot_core.egg-info/requires.txt +5 -0
  7. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/__init__.py +9 -0
  8. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/entity_ops.py +18 -1
  9. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/release.py +16 -7
  10. ssot_core-0.2.15.dev2/src/ssot_registry/api/test_execution.py +324 -0
  11. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/upgrade.py +55 -15
  12. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/certification.py +38 -13
  13. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/document.py +2 -2
  14. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/enums.py +2 -1
  15. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/registry.py +1 -0
  16. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/release.py +1 -0
  17. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/schema_version.py +1 -1
  18. ssot_core-0.2.15.dev2/src/ssot_registry/model/test.py +29 -0
  19. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/snapshots/published_snapshot.py +5 -2
  20. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/snapshots/release_snapshot.py +6 -3
  21. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/templates/registry.full.json +1 -1
  22. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/templates/registry.minimal.json +1 -1
  23. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/structure.py +33 -0
  24. ssot_core-0.2.14.dev1/src/ssot_core.egg-info/requires.txt +0 -5
  25. ssot_core-0.2.14.dev1/src/ssot_registry/model/test.py +0 -14
  26. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/setup.cfg +0 -0
  27. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_core.egg-info/dependency_links.txt +0 -0
  28. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_core.egg-info/top_level.txt +0 -0
  29. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/__init__.py +0 -0
  30. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/__main__.py +0 -0
  31. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/boundary.py +0 -0
  32. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/claims.py +0 -0
  33. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/documents.py +0 -0
  34. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/evidence.py +0 -0
  35. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/graph.py +0 -0
  36. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/init.py +0 -0
  37. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/lifecycle.py +0 -0
  38. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/load.py +0 -0
  39. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/plan.py +0 -0
  40. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/profile_eval.py +0 -0
  41. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/profile_resolution.py +0 -0
  42. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/registry.py +0 -0
  43. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/save.py +0 -0
  44. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/status_sync.py +0 -0
  45. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/api/validate.py +0 -0
  46. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/__init__.py +0 -0
  47. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/adr_cmd.py +0 -0
  48. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/boundary_cmd.py +0 -0
  49. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/claim_cmd.py +0 -0
  50. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/common.py +0 -0
  51. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/evidence_cmd.py +0 -0
  52. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/feature_cmd.py +0 -0
  53. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/graph_cmd.py +0 -0
  54. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/init_cmd.py +0 -0
  55. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/issue_cmd.py +0 -0
  56. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/main.py +0 -0
  57. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/profile_cmd.py +0 -0
  58. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/registry_cmd.py +0 -0
  59. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/release_cmd.py +0 -0
  60. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/risk_cmd.py +0 -0
  61. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/spec_cmd.py +0 -0
  62. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/test_cmd.py +0 -0
  63. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/upgrade_cmd.py +0 -0
  64. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/cli/validate_cmd.py +0 -0
  65. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/graph/__init__.py +0 -0
  66. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/graph/export_dot.py +0 -0
  67. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/graph/export_json.py +0 -0
  68. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/__init__.py +0 -0
  69. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/claim_closure.py +0 -0
  70. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/document_lifecycle.py +0 -0
  71. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/document_supersession.py +0 -0
  72. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/feature_requirements.py +0 -0
  73. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/lifecycle.py +0 -0
  74. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/profile_requirements.py +0 -0
  75. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/promotion.py +0 -0
  76. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/guards/publication.py +0 -0
  77. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/__init__.py +0 -0
  78. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/boundary.py +0 -0
  79. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/claim.py +0 -0
  80. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/evidence.py +0 -0
  81. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/feature.py +0 -0
  82. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/ids.py +0 -0
  83. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/issue.py +0 -0
  84. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/profile.py +0 -0
  85. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/model/risk.py +0 -0
  86. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/reports/__init__.py +0 -0
  87. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/reports/certification_report.py +0 -0
  88. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/reports/summary.py +0 -0
  89. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/reports/validation_report.py +0 -0
  90. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/snapshots/__init__.py +0 -0
  91. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/snapshots/boundary_snapshot.py +0 -0
  92. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/snapshots/hashing.py +0 -0
  93. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/templates/__init__.py +0 -0
  94. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/__init__.py +0 -0
  95. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/document_io.py +0 -0
  96. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/errors.py +0 -0
  97. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/formatting.py +0 -0
  98. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/fs.py +0 -0
  99. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/jcs.py +0 -0
  100. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/jsonio.py +0 -0
  101. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/util/time.py +0 -0
  102. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/__init__.py +0 -0
  103. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/bidirectional.py +0 -0
  104. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/bounds.py +0 -0
  105. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/coverage.py +0 -0
  106. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/documents.py +0 -0
  107. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/filesystem.py +0 -0
  108. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/identity.py +0 -0
  109. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/lifecycle.py +0 -0
  110. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/promotion.py +0 -0
  111. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/references.py +0 -0
  112. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/reservations.py +0 -0
  113. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev2}/src/ssot_registry/validators/tiers.py +0 -0
  114. {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.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.14.dev1
3
+ Version: 0.2.15.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
@@ -34,8 +34,8 @@ 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.14.dev1
38
- Requires-Dist: ssot-views==0.2.14.dev1
37
+ Requires-Dist: ssot-contracts==0.2.15.dev2
38
+ Requires-Dist: ssot-views==0.2.15.dev2
39
39
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
40
40
 
41
41
  <div align="center">
@@ -52,7 +52,7 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
52
52
 
53
53
  `ssot-core` is the core Python runtime package for SSOT.
54
54
 
55
- It owns the canonical registry model, core APIs for loading and mutating registries, validation and guard logic, planning and release workflows, and the domain operations that [ssot-cli](https://pypi.org/project/ssot-cli/) and [ssot-tui](https://pypi.org/project/ssot-tui/) build on top of.
55
+ It owns the canonical registry model, core APIs for loading and mutating registries, validation and guard logic, planning and release workflows, and the domain operations that [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), and [ssot-tui](https://pypi.org/project/ssot-tui/) build on top of.
56
56
 
57
57
  - GitHub: https://github.com/groupsum/ssot-registry
58
58
 
@@ -94,8 +94,8 @@ ADR and SPEC companion documents are canonically authored as JSON under `.ssot/a
94
94
 
95
95
  ```bash
96
96
  python -m pip install ssot-core # core library/runtime only
97
- python -m pip install ssot-cli # primary CLI distribution + ssot-core
98
- python -m pip install ssot-registry # umbrella bundle: ssot-core + ssot-cli
97
+ python -m pip install ssot-cli # primary CLI distribution + ssot-core + ssot-conformance
98
+ python -m pip install ssot-registry # umbrella bundle: ssot-core + ssot-cli + ssot-conformance
99
99
  python -m pip install "ssot-registry[tui]" # optional TUI bundle
100
100
  ```
101
101
 
@@ -182,6 +182,8 @@ ssot-registry registry --help
182
182
 
183
183
  CLI screenshots from [ssot-cli](https://pypi.org/project/ssot-cli/):
184
184
 
185
+ Regenerate all terminal assets with `python scripts/generate_terminal_screenshots.py`.
186
+
185
187
  ![ssot top-level help](https://raw.githubusercontent.com/groupsum/ssot-registry/main/pkgs/ssot-cli/assets/ssot-cli-help.png)
186
188
 
187
189
  ![ssot boundary help](https://raw.githubusercontent.com/groupsum/ssot-registry/main/pkgs/ssot-cli/assets/ssot-cli-boundary-help.png)
@@ -932,6 +934,6 @@ ssot-registry registry export . --format toml --output .ssot/exports/registry.to
932
934
 
933
935
  - Package type: core runtime/library package
934
936
  - Depends on: [ssot-contracts](https://pypi.org/project/ssot-contracts/), [ssot-views](https://pypi.org/project/ssot-views/)
935
- - Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-tui](https://pypi.org/project/ssot-tui/), direct Python integrations, and automation
937
+ - Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), [ssot-tui](https://pypi.org/project/ssot-tui/), direct Python integrations, and automation
936
938
 
937
939
  If you are embedding SSOT behavior inside Python code, this is the package to import. If you need the primary CLI distribution, install [ssot-cli](https://pypi.org/project/ssot-cli/) alongside it.
@@ -12,7 +12,7 @@
12
12
 
13
13
  `ssot-core` is the core Python runtime package for SSOT.
14
14
 
15
- It owns the canonical registry model, core APIs for loading and mutating registries, validation and guard logic, planning and release workflows, and the domain operations that [ssot-cli](https://pypi.org/project/ssot-cli/) and [ssot-tui](https://pypi.org/project/ssot-tui/) build on top of.
15
+ It owns the canonical registry model, core APIs for loading and mutating registries, validation and guard logic, planning and release workflows, and the domain operations that [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), and [ssot-tui](https://pypi.org/project/ssot-tui/) build on top of.
16
16
 
17
17
  - GitHub: https://github.com/groupsum/ssot-registry
18
18
 
@@ -54,8 +54,8 @@ ADR and SPEC companion documents are canonically authored as JSON under `.ssot/a
54
54
 
55
55
  ```bash
56
56
  python -m pip install ssot-core # core library/runtime only
57
- python -m pip install ssot-cli # primary CLI distribution + ssot-core
58
- python -m pip install ssot-registry # umbrella bundle: ssot-core + ssot-cli
57
+ python -m pip install ssot-cli # primary CLI distribution + ssot-core + ssot-conformance
58
+ python -m pip install ssot-registry # umbrella bundle: ssot-core + ssot-cli + ssot-conformance
59
59
  python -m pip install "ssot-registry[tui]" # optional TUI bundle
60
60
  ```
61
61
 
@@ -142,6 +142,8 @@ ssot-registry registry --help
142
142
 
143
143
  CLI screenshots from [ssot-cli](https://pypi.org/project/ssot-cli/):
144
144
 
145
+ Regenerate all terminal assets with `python scripts/generate_terminal_screenshots.py`.
146
+
145
147
  ![ssot top-level help](https://raw.githubusercontent.com/groupsum/ssot-registry/main/pkgs/ssot-cli/assets/ssot-cli-help.png)
146
148
 
147
149
  ![ssot boundary help](https://raw.githubusercontent.com/groupsum/ssot-registry/main/pkgs/ssot-cli/assets/ssot-cli-boundary-help.png)
@@ -892,6 +894,6 @@ ssot-registry registry export . --format toml --output .ssot/exports/registry.to
892
894
 
893
895
  - Package type: core runtime/library package
894
896
  - Depends on: [ssot-contracts](https://pypi.org/project/ssot-contracts/), [ssot-views](https://pypi.org/project/ssot-views/)
895
- - Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-tui](https://pypi.org/project/ssot-tui/), direct Python integrations, and automation
897
+ - Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), [ssot-tui](https://pypi.org/project/ssot-tui/), direct Python integrations, and automation
896
898
 
897
899
  If you are embedding SSOT behavior inside Python code, this is the package to import. If you need the primary CLI distribution, install [ssot-cli](https://pypi.org/project/ssot-cli/) alongside it.
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ssot-core"
7
- version = "0.2.14.dev1"
7
+ version = "0.2.15.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.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.14.dev1",
15
- "ssot-views==0.2.14.dev1",
14
+ "ssot-contracts==0.2.15.dev2",
15
+ "ssot-views==0.2.15.dev2",
16
16
  "tomli>=2.0.1; python_version < '3.11'",
17
17
  ]
18
18
  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.14.dev1
3
+ Version: 0.2.15.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
@@ -34,8 +34,8 @@ 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.14.dev1
38
- Requires-Dist: ssot-views==0.2.14.dev1
37
+ Requires-Dist: ssot-contracts==0.2.15.dev2
38
+ Requires-Dist: ssot-views==0.2.15.dev2
39
39
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
40
40
 
41
41
  <div align="center">
@@ -52,7 +52,7 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
52
52
 
53
53
  `ssot-core` is the core Python runtime package for SSOT.
54
54
 
55
- It owns the canonical registry model, core APIs for loading and mutating registries, validation and guard logic, planning and release workflows, and the domain operations that [ssot-cli](https://pypi.org/project/ssot-cli/) and [ssot-tui](https://pypi.org/project/ssot-tui/) build on top of.
55
+ It owns the canonical registry model, core APIs for loading and mutating registries, validation and guard logic, planning and release workflows, and the domain operations that [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), and [ssot-tui](https://pypi.org/project/ssot-tui/) build on top of.
56
56
 
57
57
  - GitHub: https://github.com/groupsum/ssot-registry
58
58
 
@@ -94,8 +94,8 @@ ADR and SPEC companion documents are canonically authored as JSON under `.ssot/a
94
94
 
95
95
  ```bash
96
96
  python -m pip install ssot-core # core library/runtime only
97
- python -m pip install ssot-cli # primary CLI distribution + ssot-core
98
- python -m pip install ssot-registry # umbrella bundle: ssot-core + ssot-cli
97
+ python -m pip install ssot-cli # primary CLI distribution + ssot-core + ssot-conformance
98
+ python -m pip install ssot-registry # umbrella bundle: ssot-core + ssot-cli + ssot-conformance
99
99
  python -m pip install "ssot-registry[tui]" # optional TUI bundle
100
100
  ```
101
101
 
@@ -182,6 +182,8 @@ ssot-registry registry --help
182
182
 
183
183
  CLI screenshots from [ssot-cli](https://pypi.org/project/ssot-cli/):
184
184
 
185
+ Regenerate all terminal assets with `python scripts/generate_terminal_screenshots.py`.
186
+
185
187
  ![ssot top-level help](https://raw.githubusercontent.com/groupsum/ssot-registry/main/pkgs/ssot-cli/assets/ssot-cli-help.png)
186
188
 
187
189
  ![ssot boundary help](https://raw.githubusercontent.com/groupsum/ssot-registry/main/pkgs/ssot-cli/assets/ssot-cli-boundary-help.png)
@@ -932,6 +934,6 @@ ssot-registry registry export . --format toml --output .ssot/exports/registry.to
932
934
 
933
935
  - Package type: core runtime/library package
934
936
  - Depends on: [ssot-contracts](https://pypi.org/project/ssot-contracts/), [ssot-views](https://pypi.org/project/ssot-views/)
935
- - Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-tui](https://pypi.org/project/ssot-tui/), direct Python integrations, and automation
937
+ - Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), [ssot-tui](https://pypi.org/project/ssot-tui/), direct Python integrations, and automation
936
938
 
937
939
  If you are embedding SSOT behavior inside Python code, this is the package to import. If you need the primary CLI distribution, install [ssot-cli](https://pypi.org/project/ssot-cli/) alongside it.
@@ -25,6 +25,7 @@ src/ssot_registry/api/registry.py
25
25
  src/ssot_registry/api/release.py
26
26
  src/ssot_registry/api/save.py
27
27
  src/ssot_registry/api/status_sync.py
28
+ src/ssot_registry/api/test_execution.py
28
29
  src/ssot_registry/api/upgrade.py
29
30
  src/ssot_registry/api/validate.py
30
31
  src/ssot_registry/cli/__init__.py
@@ -0,0 +1,5 @@
1
+ ssot-contracts==0.2.15.dev2
2
+ ssot-views==0.2.15.dev2
3
+
4
+ [:python_version < "3.11"]
5
+ tomli>=2.0.1
@@ -18,6 +18,7 @@ from .documents import (
18
18
  from .entity_ops import (
19
19
  add_boundary_features,
20
20
  add_boundary_profiles,
21
+ add_release_boundaries,
21
22
  add_release_claims,
22
23
  add_release_evidence,
23
24
  create_entity,
@@ -27,6 +28,7 @@ from .entity_ops import (
27
28
  list_entities,
28
29
  remove_boundary_features,
29
30
  remove_boundary_profiles,
31
+ remove_release_boundaries,
30
32
  remove_release_claims,
31
33
  remove_release_evidence,
32
34
  set_claim_status,
@@ -47,6 +49,7 @@ from .registry import export_registry
47
49
  from .release import certify_release, promote_release, publish_release, revoke_release
48
50
  from .save import save_registry
49
51
  from .status_sync import sync_automated_statuses
52
+ from .test_execution import run_boundary_tests, run_resolved_test_rows, run_spec_tests, run_tests
50
53
  from .upgrade import upgrade_registry
51
54
  from .validate import validate_registry
52
55
 
@@ -83,6 +86,8 @@ __all__ = [
83
86
  "add_boundary_profiles",
84
87
  "remove_boundary_features",
85
88
  "remove_boundary_profiles",
89
+ "add_release_boundaries",
90
+ "remove_release_boundaries",
86
91
  "add_release_claims",
87
92
  "remove_release_claims",
88
93
  "add_release_evidence",
@@ -104,5 +109,9 @@ __all__ = [
104
109
  "export_graph",
105
110
  "export_registry",
106
111
  "sync_automated_statuses",
112
+ "run_tests",
113
+ "run_spec_tests",
114
+ "run_boundary_tests",
115
+ "run_resolved_test_rows",
107
116
  "upgrade_registry",
108
117
  ]
@@ -47,7 +47,7 @@ LINKABLE_FIELDS = {
47
47
  "issues": {"feature_ids", "claim_ids", "test_ids", "evidence_ids", "risk_ids"},
48
48
  "risks": {"feature_ids", "claim_ids", "test_ids", "evidence_ids", "issue_ids"},
49
49
  "boundaries": {"feature_ids", "profile_ids"},
50
- "releases": {"claim_ids", "evidence_ids"},
50
+ "releases": {"boundary_ids", "claim_ids", "evidence_ids"},
51
51
  }
52
52
 
53
53
  SECTIONS = tuple(SECTION_LABELS)
@@ -366,3 +366,20 @@ def add_release_evidence(path: str | Path, release_id: str, evidence_ids: list[s
366
366
 
367
367
  def remove_release_evidence(path: str | Path, release_id: str, evidence_ids: list[str]) -> dict[str, Any]:
368
368
  return unlink_entities(path, "releases", release_id, {"evidence_ids": evidence_ids})
369
+
370
+
371
+ def add_release_boundaries(path: str | Path, release_id: str, boundary_ids: list[str]) -> dict[str, Any]:
372
+ _registry_path, _repo_root, registry = load_registry(path)
373
+ release = _entity_row(registry, "releases", release_id)
374
+ primary_boundary_id = release.get("boundary_id")
375
+ if isinstance(primary_boundary_id, str):
376
+ boundary_ids = [primary_boundary_id, *boundary_ids]
377
+ return link_entities(path, "releases", release_id, {"boundary_ids": boundary_ids})
378
+
379
+
380
+ def remove_release_boundaries(path: str | Path, release_id: str, boundary_ids: list[str]) -> dict[str, Any]:
381
+ _registry_path, _repo_root, registry = load_registry(path)
382
+ release = _entity_row(registry, "releases", release_id)
383
+ if release.get("boundary_id") in set(boundary_ids):
384
+ raise ValueError("Cannot remove the primary boundary_id from release boundary_ids; update the release boundary_id first")
385
+ return unlink_entities(path, "releases", release_id, {"boundary_ids": boundary_ids})
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- from ssot_registry.guards.certification import evaluate_release_certification_guard
5
+ from ssot_registry.guards.certification import evaluate_release_certification_guard, release_boundary_ids
6
6
  from ssot_registry.guards.promotion import evaluate_release_promotion_guard
7
7
  from ssot_registry.guards.publication import evaluate_release_publication_guard
8
8
  from ssot_registry.model.enums import CLAIM_STATUS_RANK
@@ -27,6 +27,10 @@ def _entity_lookup(registry: dict[str, object], section: str) -> dict[str, dict[
27
27
  return {row["id"]: row for row in registry[section]}
28
28
 
29
29
 
30
+ def _release_boundaries(release: dict[str, object], boundary_lookup: dict[str, dict[str, object]]) -> list[dict[str, object]]:
31
+ return [boundary_lookup[boundary_id] for boundary_id in release_boundary_ids(release)]
32
+
33
+
30
34
  def certify_release(path: str | Path, release_id: str | None = None, write_report: bool = False) -> dict[str, object]:
31
35
  registry_path, repo_root, registry = load_registry(path)
32
36
  preflight = validate_registry(registry_path)
@@ -85,7 +89,7 @@ def promote_release(path: str | Path, release_id: str | None = None) -> dict[str
85
89
  boundary_lookup = _entity_lookup(registry, "boundaries")
86
90
 
87
91
  release = release_lookup[selected_release_id]
88
- boundary = boundary_lookup[release["boundary_id"]]
92
+ boundaries = _release_boundaries(release, boundary_lookup)
89
93
  release["status"] = "promoted"
90
94
  for claim_id in release.get("claim_ids", []):
91
95
  if claim_id in claim_lookup and CLAIM_STATUS_RANK[claim_lookup[claim_id]["status"]] < CLAIM_STATUS_RANK["promoted"]:
@@ -98,9 +102,14 @@ def promote_release(path: str | Path, release_id: str | None = None) -> dict[str
98
102
  save_registry(registry_path, registry)
99
103
 
100
104
  release_claims = [claim_lookup[claim_id] for claim_id in release.get("claim_ids", []) if claim_id in claim_lookup]
101
- boundary_feature_ids = resolve_boundary_feature_ids(boundary, index)
105
+ boundary_feature_ids = sorted({feature_id for boundary in boundaries for feature_id in resolve_boundary_feature_ids(boundary, index)})
102
106
  boundary_features = [feature_lookup[feature_id] for feature_id in boundary_feature_ids if feature_id in feature_lookup]
103
- boundary_profiles = [index["profiles"][profile_id] for profile_id in boundary.get("profile_ids", []) if profile_id in index["profiles"]]
107
+ boundary_profiles = [
108
+ index["profiles"][profile_id]
109
+ for boundary in boundaries
110
+ for profile_id in boundary.get("profile_ids", [])
111
+ if profile_id in index["profiles"]
112
+ ]
104
113
  profile_evaluations = [evaluate_profile(profile, index, registry.get("guard_policies", {})) for profile in boundary_profiles]
105
114
  release_test_ids = sorted({test_id for claim in release_claims for test_id in claim.get("test_ids", []) if test_id in test_lookup})
106
115
  release_evidence_ids = sorted(
@@ -113,7 +122,7 @@ def promote_release(path: str | Path, release_id: str | None = None) -> dict[str
113
122
  repo_root,
114
123
  registry_path,
115
124
  release,
116
- boundary,
125
+ boundaries,
117
126
  boundary_features,
118
127
  boundary_profiles,
119
128
  profile_evaluations,
@@ -155,7 +164,7 @@ def publish_release(path: str | Path, release_id: str | None = None) -> dict[str
155
164
  boundary_lookup = _entity_lookup(registry, "boundaries")
156
165
 
157
166
  release = release_lookup[selected_release_id]
158
- boundary = boundary_lookup[release["boundary_id"]]
167
+ boundaries = _release_boundaries(release, boundary_lookup)
159
168
  release["status"] = "published"
160
169
  for claim_id in release.get("claim_ids", []):
161
170
  if claim_id in claim_lookup and CLAIM_STATUS_RANK[claim_lookup[claim_id]["status"]] < CLAIM_STATUS_RANK["published"]:
@@ -174,7 +183,7 @@ def publish_release(path: str | Path, release_id: str | None = None) -> dict[str
174
183
  repo_root,
175
184
  registry_path,
176
185
  release,
177
- boundary,
186
+ boundaries,
178
187
  claims,
179
188
  evidence,
180
189
  )
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ssot_registry.util.jcs import dump_jcs_json
10
+
11
+ from .load import load_registry
12
+ from .profile_resolution import resolve_boundary_feature_ids
13
+
14
+
15
+ def _row_lookup(registry: dict[str, Any], section: str) -> dict[str, dict[str, Any]]:
16
+ return {
17
+ row["id"]: row
18
+ for row in registry.get(section, [])
19
+ if isinstance(row, dict) and isinstance(row.get("id"), str)
20
+ }
21
+
22
+
23
+ def _dedupe_preserve(values: list[str]) -> list[str]:
24
+ seen: set[str] = set()
25
+ ordered: list[str] = []
26
+ for value in values:
27
+ if value not in seen:
28
+ seen.add(value)
29
+ ordered.append(value)
30
+ return ordered
31
+
32
+
33
+ def _validate_execution_contract(test: dict[str, Any]) -> dict[str, Any]:
34
+ execution = test.get("execution")
35
+ test_id = str(test.get("id", "<missing>"))
36
+ if not isinstance(execution, dict):
37
+ raise ValueError(f"Test {test_id} is not runnable; missing execution metadata")
38
+ if execution.get("mode") != "command":
39
+ raise ValueError(f"Test {test_id} execution.mode must be 'command'")
40
+ argv = execution.get("argv")
41
+ if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item.strip() for item in argv):
42
+ raise ValueError(f"Test {test_id} execution.argv must be a non-empty list of strings")
43
+ cwd = execution.get("cwd")
44
+ if not isinstance(cwd, str) or not cwd.strip():
45
+ raise ValueError(f"Test {test_id} execution.cwd must be a non-empty string")
46
+ env = execution.get("env")
47
+ if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()):
48
+ raise ValueError(f"Test {test_id} execution.env must be a string-to-string object")
49
+ timeout_seconds = execution.get("timeout_seconds")
50
+ if not isinstance(timeout_seconds, int) or timeout_seconds <= 0:
51
+ raise ValueError(f"Test {test_id} execution.timeout_seconds must be a positive integer")
52
+ success = execution.get("success")
53
+ if not isinstance(success, dict):
54
+ raise ValueError(f"Test {test_id} execution.success must be an object")
55
+ if success.get("type") != "exit_code":
56
+ raise ValueError(f"Test {test_id} execution.success.type must be 'exit_code'")
57
+ expected = success.get("expected")
58
+ if not isinstance(expected, int):
59
+ raise ValueError(f"Test {test_id} execution.success.expected must be an integer")
60
+ return execution
61
+
62
+
63
+ def _resolve_command_cwd(repo_root: Path, cwd: str, *, test_id: str) -> Path:
64
+ relative = Path(cwd)
65
+ if relative.is_absolute():
66
+ raise ValueError(f"Test {test_id} execution.cwd must be repo-relative")
67
+ target = (repo_root / relative).resolve()
68
+ if repo_root not in {target, *target.parents}:
69
+ raise ValueError(f"Test {test_id} execution.cwd must stay within the repo root")
70
+ return target
71
+
72
+
73
+ def _write_evidence_output(path: str | Path, payload: dict[str, Any]) -> str:
74
+ target = Path(path)
75
+ target.parent.mkdir(parents=True, exist_ok=True)
76
+ target.write_text(dump_jcs_json(payload), encoding="utf-8")
77
+ return target.as_posix()
78
+
79
+
80
+ def _build_evidence_payload(
81
+ *,
82
+ repo_root: Path,
83
+ target: dict[str, Any],
84
+ resolved_test_ids: list[str],
85
+ cases: list[dict[str, Any]],
86
+ ) -> dict[str, Any]:
87
+ summary = {"passed": 0, "failed": 0, "skipped": 0, "total": len(cases)}
88
+ for case in cases:
89
+ outcome = str(case.get("outcome", "unknown"))
90
+ if outcome in summary:
91
+ summary[outcome] += 1
92
+ return {
93
+ "kind": "ssot-conformance-evidence",
94
+ "repo_root": repo_root.as_posix(),
95
+ "profiles": [],
96
+ "families": [],
97
+ "target": target,
98
+ "resolved_test_ids": list(resolved_test_ids),
99
+ "summary": summary,
100
+ "cases": cases,
101
+ }
102
+
103
+
104
+ def _resolve_tests_from_ids(registry: dict[str, Any], test_ids: list[str]) -> list[dict[str, Any]]:
105
+ tests = _row_lookup(registry, "tests")
106
+ missing = [test_id for test_id in test_ids if test_id not in tests]
107
+ if missing:
108
+ raise ValueError(f"Unknown test ids: {', '.join(sorted(missing))}")
109
+ return [tests[test_id] for test_id in _dedupe_preserve(test_ids)]
110
+
111
+
112
+ def _resolve_tests_for_spec(registry: dict[str, Any], spec_id: str) -> tuple[list[str], list[dict[str, Any]]]:
113
+ specs = _row_lookup(registry, "specs")
114
+ if spec_id not in specs:
115
+ raise ValueError(f"Unknown spec id: {spec_id}")
116
+ feature_ids = [
117
+ row["id"]
118
+ for row in registry.get("features", [])
119
+ if isinstance(row, dict) and spec_id in row.get("spec_ids", [])
120
+ ]
121
+ resolved_test_ids: list[str] = []
122
+ for row in registry.get("features", []):
123
+ if isinstance(row, dict) and row.get("id") in feature_ids:
124
+ resolved_test_ids.extend(row.get("test_ids", []))
125
+ return feature_ids, _resolve_tests_from_ids(registry, resolved_test_ids)
126
+
127
+
128
+ def _resolve_tests_for_boundary(registry: dict[str, Any], boundary_id: str | None) -> tuple[str, list[str], list[dict[str, Any]]]:
129
+ boundaries = _row_lookup(registry, "boundaries")
130
+ if boundary_id is None:
131
+ program = registry.get("program", {})
132
+ if not isinstance(program, dict):
133
+ raise ValueError("Registry program section is malformed")
134
+ active_boundary_id = program.get("active_boundary_id")
135
+ if not isinstance(active_boundary_id, str) or not active_boundary_id.strip():
136
+ raise ValueError("Registry program.active_boundary_id is missing")
137
+ boundary_id = active_boundary_id
138
+ boundary = boundaries.get(boundary_id)
139
+ if boundary is None:
140
+ raise ValueError(f"Unknown boundary id: {boundary_id}")
141
+ index = {
142
+ "features": _row_lookup(registry, "features"),
143
+ "profiles": _row_lookup(registry, "profiles"),
144
+ }
145
+ feature_ids = list(resolve_boundary_feature_ids(boundary, index))
146
+ resolved_test_ids: list[str] = []
147
+ for row in registry.get("features", []):
148
+ if isinstance(row, dict) and row.get("id") in feature_ids:
149
+ resolved_test_ids.extend(row.get("test_ids", []))
150
+ return boundary_id, feature_ids, _resolve_tests_from_ids(registry, resolved_test_ids)
151
+
152
+
153
+ def _run_test_cases(
154
+ *,
155
+ repo_root: Path,
156
+ target: dict[str, Any],
157
+ tests: list[dict[str, Any]],
158
+ evidence_output: str | Path | None,
159
+ dry_run: bool,
160
+ ) -> dict[str, Any]:
161
+ resolved_tests = [
162
+ {
163
+ "id": row["id"],
164
+ "title": row["title"],
165
+ "kind": row["kind"],
166
+ "path": row["path"],
167
+ "execution": row.get("execution"),
168
+ }
169
+ for row in tests
170
+ ]
171
+ if dry_run:
172
+ return {
173
+ "passed": True,
174
+ "dry_run": True,
175
+ "target": target,
176
+ "resolved_tests": resolved_tests,
177
+ }
178
+
179
+ execution_specs = {row["id"]: _validate_execution_contract(row) for row in tests}
180
+ cases: list[dict[str, Any]] = []
181
+ for row in tests:
182
+ test_id = str(row["id"])
183
+ execution = execution_specs[test_id]
184
+ command = list(execution["argv"])
185
+ if command and command[0] == "python":
186
+ command[0] = sys.executable
187
+ command_cwd = _resolve_command_cwd(repo_root, str(execution["cwd"]), test_id=test_id)
188
+ env = os.environ.copy()
189
+ env.update(execution["env"])
190
+ expected_exit = int(execution["success"]["expected"])
191
+ timeout_seconds = int(execution["timeout_seconds"])
192
+
193
+ case = {
194
+ "nodeid": test_id,
195
+ "test_id": test_id,
196
+ "title": row["title"],
197
+ "kind": row["kind"],
198
+ "path": row["path"],
199
+ "runner": "command",
200
+ "command": command,
201
+ "cwd": Path(execution["cwd"]).as_posix(),
202
+ "expected_exit_code": expected_exit,
203
+ }
204
+ try:
205
+ result = subprocess.run(
206
+ command,
207
+ cwd=str(command_cwd),
208
+ env=env,
209
+ text=True,
210
+ capture_output=True,
211
+ timeout=timeout_seconds,
212
+ check=False,
213
+ )
214
+ case["returncode"] = result.returncode
215
+ case["outcome"] = "passed" if result.returncode == expected_exit else "failed"
216
+ if result.stdout:
217
+ case["stdout"] = result.stdout
218
+ if result.stderr:
219
+ case["stderr"] = result.stderr
220
+ except subprocess.TimeoutExpired as exc:
221
+ case["returncode"] = None
222
+ case["outcome"] = "failed"
223
+ case["stderr"] = f"Command timed out after {timeout_seconds} seconds"
224
+ if exc.stdout:
225
+ case["stdout"] = exc.stdout
226
+ if exc.stderr:
227
+ case["stderr_detail"] = exc.stderr
228
+ except FileNotFoundError as exc:
229
+ case["returncode"] = None
230
+ case["outcome"] = "failed"
231
+ case["stderr"] = str(exc)
232
+ cases.append(case)
233
+
234
+ resolved_test_ids = [row["id"] for row in tests]
235
+ evidence_payload = _build_evidence_payload(
236
+ repo_root=repo_root,
237
+ target=target,
238
+ resolved_test_ids=resolved_test_ids,
239
+ cases=cases,
240
+ )
241
+ passed = evidence_payload["summary"]["failed"] == 0
242
+ payload: dict[str, Any] = {
243
+ "passed": passed,
244
+ "dry_run": False,
245
+ "target": target,
246
+ "resolved_tests": resolved_tests,
247
+ "summary": evidence_payload["summary"],
248
+ }
249
+ if evidence_output is not None:
250
+ payload["evidence_output"] = _write_evidence_output(evidence_output, evidence_payload)
251
+ else:
252
+ payload["cases"] = cases
253
+ return payload
254
+
255
+
256
+ def run_resolved_test_rows(
257
+ repo_root: str | Path,
258
+ *,
259
+ target: dict[str, Any],
260
+ tests: list[dict[str, Any]],
261
+ evidence_output: str | Path | None = None,
262
+ dry_run: bool = False,
263
+ ) -> dict[str, Any]:
264
+ return _run_test_cases(
265
+ repo_root=Path(repo_root).resolve(),
266
+ target=target,
267
+ tests=tests,
268
+ evidence_output=evidence_output,
269
+ dry_run=dry_run,
270
+ )
271
+
272
+
273
+ def run_tests(
274
+ path: str | Path,
275
+ *,
276
+ test_ids: list[str],
277
+ evidence_output: str | Path | None = None,
278
+ dry_run: bool = False,
279
+ ) -> dict[str, Any]:
280
+ _registry_path, repo_root, registry = load_registry(path)
281
+ tests = _resolve_tests_from_ids(registry, test_ids)
282
+ return _run_test_cases(
283
+ repo_root=repo_root,
284
+ target={"kind": "test", "ids": [row["id"] for row in tests]},
285
+ tests=tests,
286
+ evidence_output=evidence_output,
287
+ dry_run=dry_run,
288
+ )
289
+
290
+
291
+ def run_spec_tests(
292
+ path: str | Path,
293
+ *,
294
+ spec_id: str,
295
+ evidence_output: str | Path | None = None,
296
+ dry_run: bool = False,
297
+ ) -> dict[str, Any]:
298
+ _registry_path, repo_root, registry = load_registry(path)
299
+ feature_ids, tests = _resolve_tests_for_spec(registry, spec_id)
300
+ return _run_test_cases(
301
+ repo_root=repo_root,
302
+ target={"kind": "spec", "id": spec_id, "feature_ids": feature_ids},
303
+ tests=tests,
304
+ evidence_output=evidence_output,
305
+ dry_run=dry_run,
306
+ )
307
+
308
+
309
+ def run_boundary_tests(
310
+ path: str | Path,
311
+ *,
312
+ boundary_id: str | None = None,
313
+ evidence_output: str | Path | None = None,
314
+ dry_run: bool = False,
315
+ ) -> dict[str, Any]:
316
+ _registry_path, repo_root, registry = load_registry(path)
317
+ resolved_boundary_id, feature_ids, tests = _resolve_tests_for_boundary(registry, boundary_id)
318
+ return _run_test_cases(
319
+ repo_root=repo_root,
320
+ target={"kind": "boundary", "id": resolved_boundary_id, "feature_ids": feature_ids},
321
+ tests=tests,
322
+ evidence_output=evidence_output,
323
+ dry_run=dry_run,
324
+ )