ssot-core 0.2.14.dev1__tar.gz → 0.2.15.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.
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/PKG-INFO +9 -7
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/README.md +6 -4
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/pyproject.toml +3 -3
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_core.egg-info/PKG-INFO +9 -7
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_core.egg-info/SOURCES.txt +1 -0
- ssot_core-0.2.15.dev1/src/ssot_core.egg-info/requires.txt +5 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/__init__.py +9 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/entity_ops.py +18 -1
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/release.py +16 -7
- ssot_core-0.2.15.dev1/src/ssot_registry/api/test_execution.py +324 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/upgrade.py +55 -15
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/certification.py +38 -13
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/document.py +2 -2
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/enums.py +2 -1
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/registry.py +1 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/release.py +1 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/schema_version.py +1 -1
- ssot_core-0.2.15.dev1/src/ssot_registry/model/test.py +29 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/snapshots/published_snapshot.py +5 -2
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/snapshots/release_snapshot.py +6 -3
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/templates/registry.full.json +1 -1
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/templates/registry.minimal.json +1 -1
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/structure.py +33 -0
- ssot_core-0.2.14.dev1/src/ssot_core.egg-info/requires.txt +0 -5
- ssot_core-0.2.14.dev1/src/ssot_registry/model/test.py +0 -14
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/setup.cfg +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_core.egg-info/dependency_links.txt +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_core.egg-info/top_level.txt +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/__main__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/boundary.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/claims.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/documents.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/evidence.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/graph.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/init.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/lifecycle.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/load.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/plan.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/profile_eval.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/profile_resolution.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/registry.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/save.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/status_sync.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/api/validate.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/adr_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/boundary_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/claim_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/common.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/evidence_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/feature_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/graph_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/init_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/issue_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/main.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/profile_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/registry_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/release_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/risk_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/spec_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/test_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/upgrade_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/cli/validate_cmd.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/graph/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/graph/export_dot.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/graph/export_json.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/claim_closure.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/document_lifecycle.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/document_supersession.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/feature_requirements.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/lifecycle.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/profile_requirements.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/promotion.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/guards/publication.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/boundary.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/claim.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/evidence.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/feature.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/ids.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/issue.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/profile.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/model/risk.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/reports/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/reports/certification_report.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/reports/summary.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/reports/validation_report.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/snapshots/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/snapshots/boundary_snapshot.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/snapshots/hashing.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/templates/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/document_io.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/errors.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/formatting.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/fs.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/jcs.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/jsonio.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/util/time.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/__init__.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/bidirectional.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/bounds.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/coverage.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/documents.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/filesystem.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/identity.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/lifecycle.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/promotion.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/references.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/reservations.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.dev1}/src/ssot_registry/validators/tiers.py +0 -0
- {ssot_core-0.2.14.dev1 → ssot_core-0.2.15.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.
|
|
3
|
+
Version: 0.2.15.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,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.
|
|
38
|
-
Requires-Dist: ssot-views==0.2.
|
|
37
|
+
Requires-Dist: ssot-contracts==0.2.15.dev1
|
|
38
|
+
Requires-Dist: ssot-views==0.2.15.dev1
|
|
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
|

|
|
186
188
|
|
|
187
189
|

|
|
@@ -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
|

|
|
146
148
|
|
|
147
149
|

|
|
@@ -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.
|
|
7
|
+
version = "0.2.15.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.
|
|
15
|
-
"ssot-views==0.2.
|
|
14
|
+
"ssot-contracts==0.2.15.dev1",
|
|
15
|
+
"ssot-views==0.2.15.dev1",
|
|
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.
|
|
3
|
+
Version: 0.2.15.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,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.
|
|
38
|
-
Requires-Dist: ssot-views==0.2.
|
|
37
|
+
Requires-Dist: ssot-contracts==0.2.15.dev1
|
|
38
|
+
Requires-Dist: ssot-views==0.2.15.dev1
|
|
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
|

|
|
186
188
|
|
|
187
189
|

|
|
@@ -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
|
|
@@ -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
|
-
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|