ssot-core 0.2.18.dev9__tar.gz → 0.2.19.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.18.dev9 → ssot_core-0.2.19.dev1}/PKG-INFO +6 -6
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/README.md +2 -2
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/pyproject.toml +4 -4
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_core.egg-info/PKG-INFO +6 -6
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_core.egg-info/SOURCES.txt +19 -1
- ssot_core-0.2.19.dev1/src/ssot_core.egg-info/requires.txt +7 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/acl/__init__.py +5 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/acl/policy.py +92 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/acl/wrapper.py +36 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/__init__.py +8 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/entity_ops.py +92 -1
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/origin.py +8 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/upgrade.py +39 -5
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/validate.py +2 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/__init__.py +6 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/events.py +17 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/models.py +42 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/paths.py +66 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/scaffold.py +165 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/service.py +605 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/sqlite_store.py +653 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/control/sse.py +52 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/guards/completion.py +85 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/maturation/__init__.py +21 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/maturation/selector.py +320 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/enums.py +2 -1
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/feature.py +1 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/schema_version.py +1 -1
- ssot_core-0.2.19.dev1/src/ssot_registry/templates/registry.full.json +1 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/templates/registry.minimal.json +1 -1
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/__init__.py +2 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/validators/feature_parent_links.py +49 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/structure.py +3 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/watch/__init__.py +5 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/watch/git_status.py +29 -0
- ssot_core-0.2.19.dev1/src/ssot_registry/watch/observer.py +95 -0
- ssot_core-0.2.18.dev9/src/ssot_core.egg-info/requires.txt +0 -7
- ssot_core-0.2.18.dev9/src/ssot_registry/templates/registry.full.json +0 -1
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/setup.cfg +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_core.egg-info/dependency_links.txt +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_core.egg-info/top_level.txt +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/__main__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/boundary.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/claims.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/config.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/documents.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/evidence.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/graph.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/init.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/lifecycle.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/load.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/local_assurance.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/packs.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/plan.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/profile_eval.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/profile_resolution.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/registry.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/release.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/save.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/status_sync.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/api/test_execution.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/adr_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/boundary_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/claim_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/common.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/evidence_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/feature_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/graph_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/init_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/issue_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/main.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/profile_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/registry_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/release_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/risk_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/spec_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/test_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/upgrade_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/cli/validate_cmd.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/graph/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/graph/export_dot.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/graph/export_json.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/certification.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/claim_closure.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/claim_tier_gates.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/document_lifecycle.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/document_supersession.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/feature_claims.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/feature_requirements.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/lifecycle.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/profile_requirements.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/promotion.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/guards/publication.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/boundary.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/claim.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/document.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/evidence.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/ids.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/issue.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/profile.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/registry.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/release.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/risk.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/model/test.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/reports/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/reports/certification_report.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/reports/summary.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/reports/validation_report.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/snapshots/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/snapshots/boundary_snapshot.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/snapshots/hashing.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/snapshots/published_snapshot.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/snapshots/release_snapshot.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/templates/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/__init__.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/document_io.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/errors.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/formatting.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/fs.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/jcs.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/jsonio.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/registry_lock.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/util/time.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/bidirectional.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/bounds.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/claim_lineage.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/coverage.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/documents.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/filesystem.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/identity.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/lifecycle.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/origin.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/promotion.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/references.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/reservations.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.dev1}/src/ssot_registry/validators/tiers.py +0 -0
- {ssot_core-0.2.18.dev9 → ssot_core-0.2.19.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.19.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
|
|
@@ -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.
|
|
40
|
-
Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.
|
|
41
|
-
Requires-Dist: ssot-views==0.2.
|
|
39
|
+
Requires-Dist: ssot-contracts==0.2.19.dev1
|
|
40
|
+
Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.20.dev1
|
|
41
|
+
Requires-Dist: ssot-views==0.2.19.dev1
|
|
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.
|
|
56
|
-
<img src="https://img.shields.io/badge/migration%20coverage-
|
|
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.
|
|
13
|
-
<img src="https://img.shields.io/badge/migration%20coverage-
|
|
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.
|
|
7
|
+
version = "0.2.19.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.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.
|
|
16
|
-
"ssot-pack-contracts>=0.2.
|
|
17
|
-
"ssot-views==0.2.
|
|
15
|
+
"ssot-contracts==0.2.19.dev1",
|
|
16
|
+
"ssot-pack-contracts>=0.2.20.dev1,<0.3.0",
|
|
17
|
+
"ssot-views==0.2.19.dev1",
|
|
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.
|
|
3
|
+
Version: 0.2.19.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
|
|
@@ -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.
|
|
40
|
-
Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.
|
|
41
|
-
Requires-Dist: ssot-views==0.2.
|
|
39
|
+
Requires-Dist: ssot-contracts==0.2.19.dev1
|
|
40
|
+
Requires-Dist: ssot-pack-contracts<0.3.0,>=0.2.20.dev1
|
|
41
|
+
Requires-Dist: ssot-views==0.2.19.dev1
|
|
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.
|
|
56
|
-
<img src="https://img.shields.io/badge/migration%20coverage-
|
|
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,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,
|
|
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,
|
|
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"] =
|
|
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,
|
|
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,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)
|