ssot-core 0.2.15.dev1__tar.gz → 0.2.16.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.15.dev1 → ssot_core-0.2.16.dev1}/PKG-INFO +3 -3
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/pyproject.toml +3 -3
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_core.egg-info/PKG-INFO +3 -3
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_core.egg-info/SOURCES.txt +1 -0
- ssot_core-0.2.16.dev1/src/ssot_core.egg-info/requires.txt +5 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/init.py +2 -2
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/save.py +2 -2
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/status_sync.py +27 -2
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/__init__.py +5 -1
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/errors.py +4 -0
- ssot_core-0.2.16.dev1/src/ssot_registry/util/registry_lock.py +137 -0
- ssot_core-0.2.15.dev1/src/ssot_core.egg-info/requires.txt +0 -5
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/README.md +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/setup.cfg +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_core.egg-info/dependency_links.txt +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_core.egg-info/top_level.txt +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/__main__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/boundary.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/claims.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/documents.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/entity_ops.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/evidence.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/graph.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/lifecycle.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/load.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/plan.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/profile_eval.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/profile_resolution.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/registry.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/release.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/test_execution.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/upgrade.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/api/validate.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/adr_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/boundary_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/claim_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/common.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/evidence_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/feature_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/graph_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/init_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/issue_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/main.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/profile_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/registry_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/release_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/risk_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/spec_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/test_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/upgrade_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/cli/validate_cmd.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/graph/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/graph/export_dot.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/graph/export_json.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/certification.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/claim_closure.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/document_lifecycle.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/document_supersession.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/feature_requirements.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/lifecycle.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/profile_requirements.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/promotion.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/publication.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/boundary.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/claim.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/document.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/enums.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/evidence.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/feature.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/ids.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/issue.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/profile.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/registry.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/release.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/risk.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/schema_version.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/model/test.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/reports/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/reports/certification_report.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/reports/summary.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/reports/validation_report.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/boundary_snapshot.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/hashing.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/published_snapshot.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/release_snapshot.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/templates/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/templates/registry.full.json +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/templates/registry.minimal.json +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/document_io.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/formatting.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/fs.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/jcs.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/jsonio.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/util/time.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/__init__.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/bidirectional.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/bounds.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/coverage.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/documents.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/filesystem.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/identity.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/lifecycle.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/promotion.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/references.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/reservations.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/structure.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/tiers.py +0 -0
- {ssot_core-0.2.15.dev1 → ssot_core-0.2.16.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.16.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.16.dev1
|
|
38
|
+
Requires-Dist: ssot-views==0.2.16.dev1
|
|
39
39
|
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
40
40
|
|
|
41
41
|
<div align="center">
|
|
@@ -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.16.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.16.dev1",
|
|
15
|
+
"ssot-views==0.2.16.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.16.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.16.dev1
|
|
38
|
+
Requires-Dist: ssot-views==0.2.16.dev1
|
|
39
39
|
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
40
40
|
|
|
41
41
|
<div align="center">
|
|
@@ -94,6 +94,7 @@ src/ssot_registry/util/formatting.py
|
|
|
94
94
|
src/ssot_registry/util/fs.py
|
|
95
95
|
src/ssot_registry/util/jcs.py
|
|
96
96
|
src/ssot_registry/util/jsonio.py
|
|
97
|
+
src/ssot_registry/util/registry_lock.py
|
|
97
98
|
src/ssot_registry/util/time.py
|
|
98
99
|
src/ssot_registry/validators/__init__.py
|
|
99
100
|
src/ssot_registry/validators/bidirectional.py
|
|
@@ -6,7 +6,7 @@ from ssot_contracts.schema import list_schema_names, load_schema_text
|
|
|
6
6
|
from ssot_registry.api.documents import sync_all_documents
|
|
7
7
|
from ssot_registry.model.registry import build_minimal_registry, default_paths
|
|
8
8
|
from ssot_registry.util.errors import RegistryError
|
|
9
|
-
from
|
|
9
|
+
from .save import save_registry
|
|
10
10
|
from .validate import validate_registry
|
|
11
11
|
|
|
12
12
|
|
|
@@ -36,7 +36,7 @@ def initialize_repo(
|
|
|
36
36
|
(repo_root / relative_path).mkdir(parents=True, exist_ok=True)
|
|
37
37
|
|
|
38
38
|
registry = build_minimal_registry(repo_id, repo_name, version)
|
|
39
|
-
|
|
39
|
+
save_registry(registry_path, registry)
|
|
40
40
|
|
|
41
41
|
_copy_schema_tree(repo_root / paths["schema_root"])
|
|
42
42
|
sync_all_documents(registry_path)
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from ssot_registry.util.
|
|
5
|
+
from ssot_registry.util.registry_lock import save_registry_json_locked
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def save_registry(registry_path: str | Path, registry: dict[str, object]) -> None:
|
|
9
|
-
|
|
9
|
+
save_registry_json_locked(registry_path, registry)
|
|
@@ -18,6 +18,13 @@ CLAIM_TERMINAL_STATUSES = {"retired"}
|
|
|
18
18
|
CLAIM_RELEASE_STATUSES = {"promoted", "published"}
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _is_planned_path(path: object, marker: str) -> bool:
|
|
22
|
+
if not isinstance(path, str):
|
|
23
|
+
return False
|
|
24
|
+
normalized = path.replace("\\", "/")
|
|
25
|
+
return marker in normalized
|
|
26
|
+
|
|
27
|
+
|
|
21
28
|
def _build_current_index(registry: dict[str, Any]) -> dict[str, dict[str, dict[str, object]]]:
|
|
22
29
|
failures: list[str] = []
|
|
23
30
|
return build_index(registry, failures)
|
|
@@ -65,7 +72,10 @@ def _sync_evidence(registry: dict[str, Any], repo_root: Path) -> list[dict[str,
|
|
|
65
72
|
evidence_id = evidence["id"]
|
|
66
73
|
reason = "evidence artifact exists and linked claim tiers are satisfied"
|
|
67
74
|
status = "passed"
|
|
68
|
-
if
|
|
75
|
+
if _is_planned_path(evidence.get("path"), "/evidence/planned/"):
|
|
76
|
+
status = "planned"
|
|
77
|
+
reason = "evidence path is a planned placeholder"
|
|
78
|
+
elif not (repo_root / evidence["path"]).exists():
|
|
69
79
|
status = "planned"
|
|
70
80
|
reason = "evidence artifact path does not exist"
|
|
71
81
|
elif _missing_refs(evidence, "claim_ids", index["claims"]) or _missing_refs(evidence, "test_ids", index["tests"]):
|
|
@@ -95,7 +105,10 @@ def _sync_tests(registry: dict[str, Any], repo_root: Path) -> list[dict[str, obj
|
|
|
95
105
|
test_id = test["id"]
|
|
96
106
|
reason = "test path exists and linked evidence is passed"
|
|
97
107
|
status = "passing"
|
|
98
|
-
if
|
|
108
|
+
if _is_planned_path(test.get("path"), "tests/planned/"):
|
|
109
|
+
status = "planned"
|
|
110
|
+
reason = "test path is a planned placeholder"
|
|
111
|
+
elif not (repo_root / test["path"]).exists():
|
|
99
112
|
status = "planned"
|
|
100
113
|
reason = "test path does not exist"
|
|
101
114
|
elif _missing_refs(test, "feature_ids", index["features"]) or _missing_refs(test, "claim_ids", index["claims"]):
|
|
@@ -133,6 +146,10 @@ def _claim_support_status(claim: dict[str, Any], index: dict[str, dict[str, dict
|
|
|
133
146
|
linked_evidence = [index["evidence"][evidence_id] for evidence_id in claim.get("evidence_ids", []) if evidence_id in index["evidence"]]
|
|
134
147
|
if linked_evidence and any(evidence.get("status") in {"failed", "stale"} for evidence in linked_evidence):
|
|
135
148
|
return "blocked", "claim has failed or stale linked evidence"
|
|
149
|
+
if (linked_tests or linked_evidence) and not any(test.get("status") != "planned" for test in linked_tests) and not any(
|
|
150
|
+
evidence.get("status") != "planned" for evidence in linked_evidence
|
|
151
|
+
):
|
|
152
|
+
return "proposed", "claim has only planned verification support"
|
|
136
153
|
if linked_tests and all(test.get("status") == "passing" for test in linked_tests) and linked_evidence:
|
|
137
154
|
tier_met = any(
|
|
138
155
|
evidence.get("status") == "passed" and CLAIM_TIER_RANK[evidence["tier"]] >= CLAIM_TIER_RANK[claim["tier"]]
|
|
@@ -191,9 +208,17 @@ def _sync_features_once(registry: dict[str, Any]) -> list[dict[str, object]]:
|
|
|
191
208
|
tests_pass = bool(linked_tests) and all(test.get("status") == "passing" for test in linked_tests)
|
|
192
209
|
claims_pass = any(_claim_satisfies_feature(claim, required_tier) for claim in linked_claims)
|
|
193
210
|
requirements_pass = all(required.get("implementation_status") == "implemented" for required in required_features)
|
|
211
|
+
only_planned_support = (
|
|
212
|
+
bool(linked_tests or linked_claims)
|
|
213
|
+
and all(test.get("status") == "planned" for test in linked_tests)
|
|
214
|
+
and all(CLAIM_STATUS_RANK.get(claim.get("status"), -999) <= CLAIM_STATUS_RANK["proposed"] for claim in linked_claims)
|
|
215
|
+
)
|
|
194
216
|
if tests_pass and claims_pass and requirements_pass:
|
|
195
217
|
status = "implemented"
|
|
196
218
|
reason = "feature has passing tests, satisfying claims, and implemented requirements"
|
|
219
|
+
elif only_planned_support:
|
|
220
|
+
status = "absent"
|
|
221
|
+
reason = "feature has only planned verification support"
|
|
197
222
|
elif linked_tests or linked_claims or required_features:
|
|
198
223
|
status = "partial"
|
|
199
224
|
reason = "feature has linked support but does not yet satisfy implementation criteria"
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
from .errors import GuardError, RegistryError, ValidationError
|
|
1
|
+
from .errors import GuardError, RegistryError, RegistryLockError, ValidationError
|
|
2
2
|
from .fs import ensure_directory, resolve_registry_path, repo_root_from_registry_path, sha256_path
|
|
3
3
|
from .jsonio import load_json, save_json, stable_json_dumps
|
|
4
|
+
from .registry_lock import RegistryFileLock, registry_lock_path
|
|
4
5
|
from .time import utc_now_iso
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"GuardError",
|
|
8
9
|
"RegistryError",
|
|
10
|
+
"RegistryLockError",
|
|
9
11
|
"ValidationError",
|
|
12
|
+
"RegistryFileLock",
|
|
10
13
|
"ensure_directory",
|
|
11
14
|
"load_json",
|
|
15
|
+
"registry_lock_path",
|
|
12
16
|
"repo_root_from_registry_path",
|
|
13
17
|
"resolve_registry_path",
|
|
14
18
|
"save_json",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from contextlib import AbstractContextManager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ssot_registry.util.errors import RegistryLockError
|
|
14
|
+
from ssot_registry.util.jsonio import stable_json_dumps
|
|
15
|
+
|
|
16
|
+
DEFAULT_REGISTRY_LOCK_TTL_SECONDS = 300
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _utc_epoch_seconds() -> float:
|
|
20
|
+
return time.time()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def registry_lock_path(registry_path: str | Path) -> Path:
|
|
24
|
+
return Path(registry_path).with_name(f"{Path(registry_path).name}.lock")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_lock_metadata(lock_path: Path) -> dict[str, Any]:
|
|
28
|
+
try:
|
|
29
|
+
payload = json.loads(lock_path.read_text(encoding="utf-8"))
|
|
30
|
+
except (OSError, json.JSONDecodeError):
|
|
31
|
+
return {}
|
|
32
|
+
return payload if isinstance(payload, dict) else {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_stale(lock_path: Path, now: float) -> bool:
|
|
36
|
+
metadata = _read_lock_metadata(lock_path)
|
|
37
|
+
expires_at = metadata.get("expires_at_epoch")
|
|
38
|
+
if isinstance(expires_at, int | float):
|
|
39
|
+
return float(expires_at) <= now
|
|
40
|
+
try:
|
|
41
|
+
stat = lock_path.stat()
|
|
42
|
+
except OSError:
|
|
43
|
+
return False
|
|
44
|
+
return stat.st_mtime + DEFAULT_REGISTRY_LOCK_TTL_SECONDS <= now
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class RegistryFileLock(AbstractContextManager["RegistryFileLock"]):
|
|
49
|
+
registry_path: Path
|
|
50
|
+
reason: str = "registry mutation"
|
|
51
|
+
ttl_seconds: int = DEFAULT_REGISTRY_LOCK_TTL_SECONDS
|
|
52
|
+
owner: str | None = None
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
self.registry_path = Path(self.registry_path)
|
|
56
|
+
self.lock_path = registry_lock_path(self.registry_path)
|
|
57
|
+
self.token = uuid.uuid4().hex
|
|
58
|
+
self.owner = self.owner or f"pid:{os.getpid()}"
|
|
59
|
+
self._acquired = False
|
|
60
|
+
|
|
61
|
+
def __enter__(self) -> "RegistryFileLock":
|
|
62
|
+
self.acquire()
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
|
|
66
|
+
self.release()
|
|
67
|
+
|
|
68
|
+
def acquire(self) -> None:
|
|
69
|
+
self.lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
now = _utc_epoch_seconds()
|
|
71
|
+
metadata = {
|
|
72
|
+
"target": self.registry_path.as_posix(),
|
|
73
|
+
"owner": self.owner,
|
|
74
|
+
"pid": os.getpid(),
|
|
75
|
+
"reason": self.reason,
|
|
76
|
+
"token": self.token,
|
|
77
|
+
"created_at_epoch": now,
|
|
78
|
+
"expires_at_epoch": now + self.ttl_seconds,
|
|
79
|
+
}
|
|
80
|
+
payload = stable_json_dumps(metadata)
|
|
81
|
+
while True:
|
|
82
|
+
try:
|
|
83
|
+
fd = os.open(str(self.lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
84
|
+
except FileExistsError as exc:
|
|
85
|
+
if _is_stale(self.lock_path, _utc_epoch_seconds()):
|
|
86
|
+
try:
|
|
87
|
+
self.lock_path.unlink()
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
continue
|
|
90
|
+
except OSError as unlink_error:
|
|
91
|
+
raise RegistryLockError(f"Failed to remove stale registry lock {self.lock_path}: {unlink_error}") from unlink_error
|
|
92
|
+
continue
|
|
93
|
+
holder = _read_lock_metadata(self.lock_path)
|
|
94
|
+
owner = holder.get("owner", "unknown")
|
|
95
|
+
reason = holder.get("reason", "unknown")
|
|
96
|
+
raise RegistryLockError(f"Registry is locked by {owner} for {reason}: {self.lock_path}") from exc
|
|
97
|
+
with os.fdopen(fd, "w", encoding="utf-8") as lock_file:
|
|
98
|
+
lock_file.write(payload)
|
|
99
|
+
lock_file.flush()
|
|
100
|
+
os.fsync(lock_file.fileno())
|
|
101
|
+
self._acquired = True
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
def release(self) -> None:
|
|
105
|
+
if not self._acquired:
|
|
106
|
+
return
|
|
107
|
+
metadata = _read_lock_metadata(self.lock_path)
|
|
108
|
+
if metadata.get("token") != self.token:
|
|
109
|
+
raise RegistryLockError(f"Registry lock owner mismatch; refusing to release {self.lock_path}")
|
|
110
|
+
try:
|
|
111
|
+
self.lock_path.unlink()
|
|
112
|
+
except FileNotFoundError:
|
|
113
|
+
pass
|
|
114
|
+
self._acquired = False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def save_registry_json_locked(registry_path: str | Path, registry: dict[str, object], *, reason: str = "registry mutation") -> None:
|
|
118
|
+
target = Path(registry_path)
|
|
119
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
with RegistryFileLock(target, reason=reason):
|
|
121
|
+
payload = stable_json_dumps(registry)
|
|
122
|
+
temp_name: str | None = None
|
|
123
|
+
try:
|
|
124
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=target.parent, prefix=f".{target.name}.", suffix=".tmp", delete=False) as temp_file:
|
|
125
|
+
temp_name = temp_file.name
|
|
126
|
+
temp_file.write(payload)
|
|
127
|
+
temp_file.flush()
|
|
128
|
+
os.fsync(temp_file.fileno())
|
|
129
|
+
os.replace(temp_name, target)
|
|
130
|
+
finally:
|
|
131
|
+
if temp_name is not None:
|
|
132
|
+
temp_path = Path(temp_name)
|
|
133
|
+
if temp_path.exists():
|
|
134
|
+
try:
|
|
135
|
+
temp_path.unlink()
|
|
136
|
+
except OSError:
|
|
137
|
+
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/document_lifecycle.py
RENAMED
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/document_supersession.py
RENAMED
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/feature_requirements.py
RENAMED
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/guards/profile_requirements.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/reports/certification_report.py
RENAMED
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/reports/validation_report.py
RENAMED
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/boundary_snapshot.py
RENAMED
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/published_snapshot.py
RENAMED
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/snapshots/release_snapshot.py
RENAMED
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/templates/registry.full.json
RENAMED
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/templates/registry.minimal.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/bidirectional.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ssot_core-0.2.15.dev1 → ssot_core-0.2.16.dev1}/src/ssot_registry/validators/reservations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|