ssot-core 0.2.16.dev1__tar.gz → 0.2.17.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.16.dev1 → ssot_core-0.2.17.dev1}/PKG-INFO +4 -3
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/pyproject.toml +4 -3
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/PKG-INFO +4 -3
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/SOURCES.txt +1 -0
- ssot_core-0.2.17.dev1/src/ssot_core.egg-info/requires.txt +6 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/documents.py +67 -10
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/profile_eval.py +12 -9
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/status_sync.py +12 -7
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/upgrade.py +31 -5
- ssot_core-0.2.17.dev1/src/ssot_registry/guards/feature_claims.py +57 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/__init__.py +8 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/boundary.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/claim.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/document.py +59 -4
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/evidence.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/feature.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/issue.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/profile.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/registry.py +1 -1
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/release.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/risk.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/schema_version.py +1 -1
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/test.py +1 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/templates/registry.full.json +1 -1
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/templates/registry.minimal.json +1 -1
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/document_io.py +12 -4
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/registry_lock.py +10 -2
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/documents.py +14 -7
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/structure.py +27 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/tiers.py +3 -0
- ssot_core-0.2.16.dev1/src/ssot_core.egg-info/requires.txt +0 -5
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/README.md +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/setup.cfg +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/dependency_links.txt +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_core.egg-info/top_level.txt +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/__main__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/boundary.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/claims.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/entity_ops.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/evidence.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/graph.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/init.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/lifecycle.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/load.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/plan.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/profile_resolution.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/registry.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/release.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/save.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/test_execution.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/api/validate.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/adr_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/boundary_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/claim_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/common.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/evidence_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/feature_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/graph_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/init_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/issue_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/main.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/profile_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/registry_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/release_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/risk_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/spec_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/test_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/upgrade_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/cli/validate_cmd.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/graph/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/graph/export_dot.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/graph/export_json.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/certification.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/claim_closure.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/document_lifecycle.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/document_supersession.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/feature_requirements.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/lifecycle.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/profile_requirements.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/promotion.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/guards/publication.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/enums.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/model/ids.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/certification_report.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/summary.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/reports/validation_report.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/boundary_snapshot.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/hashing.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/published_snapshot.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/snapshots/release_snapshot.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/templates/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/errors.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/formatting.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/fs.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/jcs.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/jsonio.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/util/time.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/__init__.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/bidirectional.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/bounds.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/coverage.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/filesystem.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/identity.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/lifecycle.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/promotion.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/references.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.dev1}/src/ssot_registry/validators/reservations.py +0 -0
- {ssot_core-0.2.16.dev1 → ssot_core-0.2.17.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.17.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,9 @@ 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:
|
|
38
|
-
Requires-Dist: ssot-
|
|
37
|
+
Requires-Dist: orjson<4.0,>=3.10
|
|
38
|
+
Requires-Dist: ssot-contracts==0.2.17.dev1
|
|
39
|
+
Requires-Dist: ssot-views==0.2.17.dev1
|
|
39
40
|
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
40
41
|
|
|
41
42
|
<div align="center">
|
|
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ssot-core"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.17.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
|
-
"
|
|
15
|
-
"ssot-
|
|
14
|
+
"orjson>=3.10,<4.0",
|
|
15
|
+
"ssot-contracts==0.2.17.dev1",
|
|
16
|
+
"ssot-views==0.2.17.dev1",
|
|
16
17
|
"tomli>=2.0.1; python_version < '3.11'",
|
|
17
18
|
]
|
|
18
19
|
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.17.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,9 @@ 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:
|
|
38
|
-
Requires-Dist: ssot-
|
|
37
|
+
Requires-Dist: orjson<4.0,>=3.10
|
|
38
|
+
Requires-Dist: ssot-contracts==0.2.17.dev1
|
|
39
|
+
Requires-Dist: ssot-views==0.2.17.dev1
|
|
39
40
|
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
40
41
|
|
|
41
42
|
<div align="center">
|
|
@@ -55,6 +55,7 @@ src/ssot_registry/guards/certification.py
|
|
|
55
55
|
src/ssot_registry/guards/claim_closure.py
|
|
56
56
|
src/ssot_registry/guards/document_lifecycle.py
|
|
57
57
|
src/ssot_registry/guards/document_supersession.py
|
|
58
|
+
src/ssot_registry/guards/feature_claims.py
|
|
58
59
|
src/ssot_registry/guards/feature_requirements.py
|
|
59
60
|
src/ssot_registry/guards/lifecycle.py
|
|
60
61
|
src/ssot_registry/guards/profile_requirements.py
|
|
@@ -12,14 +12,17 @@ from ssot_registry.guards.document_lifecycle import (
|
|
|
12
12
|
)
|
|
13
13
|
from ssot_registry.guards.document_supersession import apply_supersession
|
|
14
14
|
from ssot_registry.model.document import (
|
|
15
|
+
DOCUMENT_ORIGINS,
|
|
15
16
|
DOCUMENT_STATUSES,
|
|
16
17
|
DOCUMENT_SLUG_PATTERN,
|
|
18
|
+
EXTENSION_PACK_RESERVATION_PREFIX,
|
|
17
19
|
SPEC_KINDS,
|
|
18
20
|
build_document_path,
|
|
19
21
|
default_document_id_reservations,
|
|
22
|
+
is_extension_pack_reservation_owner,
|
|
20
23
|
load_document_manifest as load_packaged_document_manifest,
|
|
21
24
|
normalize_document_id,
|
|
22
|
-
|
|
25
|
+
read_manifest_document_bytes,
|
|
23
26
|
read_packaged_document_text,
|
|
24
27
|
reservation_kind_key,
|
|
25
28
|
section_for_document_kind,
|
|
@@ -199,6 +202,53 @@ def _manifest_row_to_registry_row(registry: dict[str, Any], kind: str, manifest_
|
|
|
199
202
|
return row
|
|
200
203
|
|
|
201
204
|
|
|
205
|
+
def _packaged_manifest_entries(kind: str) -> list[dict[str, Any]]:
|
|
206
|
+
manifest_entries = load_packaged_document_manifest(kind, include_untrusted=True)
|
|
207
|
+
untrusted_extension_catalogs = sorted(
|
|
208
|
+
{
|
|
209
|
+
str(entry.get("catalog_id", ""))
|
|
210
|
+
for entry in manifest_entries
|
|
211
|
+
if entry.get("origin") == "extension-pack" and not bool(entry.get("trusted", False))
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
if untrusted_extension_catalogs:
|
|
215
|
+
raise ValidationError(
|
|
216
|
+
"Untrusted extension-pack catalogs are available but not allowed for sync: "
|
|
217
|
+
+ ", ".join(untrusted_extension_catalogs)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
trusted_entries = [entry for entry in manifest_entries if bool(entry.get("trusted", False))]
|
|
221
|
+
seen_ids: dict[str, str] = {}
|
|
222
|
+
seen_numbers: dict[int, str] = {}
|
|
223
|
+
seen_paths: dict[str, str] = {}
|
|
224
|
+
for entry in trusted_entries:
|
|
225
|
+
entry_id = str(entry.get("id", ""))
|
|
226
|
+
catalog_id = str(entry.get("catalog_id", ""))
|
|
227
|
+
number = entry.get("number")
|
|
228
|
+
target_path = str(entry.get("target_path", ""))
|
|
229
|
+
origin = entry.get("origin")
|
|
230
|
+
if origin not in {"ssot-origin", "extension-pack"}:
|
|
231
|
+
raise ValidationError(f"Packaged {kind} manifest entry {entry_id} must use origin 'ssot-origin' or 'extension-pack'")
|
|
232
|
+
if entry_id in seen_ids:
|
|
233
|
+
raise ValidationError(
|
|
234
|
+
f"Packaged {kind} id conflict: {entry_id} is present in both {seen_ids[entry_id]} and {catalog_id}"
|
|
235
|
+
)
|
|
236
|
+
if isinstance(number, int) and number in seen_numbers:
|
|
237
|
+
raise ValidationError(
|
|
238
|
+
f"Packaged {kind} number conflict: {number:04d} is present in both {seen_numbers[number]} and {catalog_id}"
|
|
239
|
+
)
|
|
240
|
+
if target_path and target_path in seen_paths:
|
|
241
|
+
raise ValidationError(
|
|
242
|
+
f"Packaged {kind} target_path conflict: {target_path} is present in both {seen_paths[target_path]} and {catalog_id}"
|
|
243
|
+
)
|
|
244
|
+
seen_ids[entry_id] = catalog_id
|
|
245
|
+
if isinstance(number, int):
|
|
246
|
+
seen_numbers[number] = catalog_id
|
|
247
|
+
if target_path:
|
|
248
|
+
seen_paths[target_path] = catalog_id
|
|
249
|
+
return trusted_entries
|
|
250
|
+
|
|
251
|
+
|
|
202
252
|
def _existing_numbers(registry: dict[str, Any], kind: str) -> set[int]:
|
|
203
253
|
return {
|
|
204
254
|
row["number"]
|
|
@@ -318,7 +368,7 @@ def create_document(
|
|
|
318
368
|
spec_kind: str | None = None,
|
|
319
369
|
adr_ids: list[str] | None = None,
|
|
320
370
|
) -> dict[str, Any]:
|
|
321
|
-
if origin not in
|
|
371
|
+
if origin not in DOCUMENT_ORIGINS:
|
|
322
372
|
raise ValidationError(f"Unsupported origin '{origin}' for {_document_label(kind)} creation")
|
|
323
373
|
if not isinstance(title, str) or not title.strip():
|
|
324
374
|
raise ValidationError(f"{_document_label(kind)} title must be a non-empty string")
|
|
@@ -335,24 +385,33 @@ def create_document(
|
|
|
335
385
|
repo_kind = _repo_kind(registry)
|
|
336
386
|
if repo_kind == "repo-local" and origin != "repo-local":
|
|
337
387
|
raise ValidationError(f"Repo-local repositories may only create repo-local {_document_label(kind)} rows")
|
|
338
|
-
if repo_kind in {"ssot-core", "ssot-origin"} and origin
|
|
388
|
+
if repo_kind in {"ssot-core", "ssot-origin"} and origin not in {"ssot-core", "ssot-origin"}:
|
|
339
389
|
raise ValidationError(f"{repo_kind} repositories may only create ssot-core or ssot-origin {_document_label(kind)} rows")
|
|
390
|
+
if repo_kind == "extension-pack" and origin != "extension-pack":
|
|
391
|
+
raise ValidationError(f"extension-pack repositories may only create extension-pack {_document_label(kind)} rows")
|
|
340
392
|
if number is None:
|
|
341
393
|
if repo_kind in {"ssot-core", "ssot-origin"} and origin in {"ssot-core", "ssot-origin"}:
|
|
342
394
|
raise ValidationError(f"{repo_kind} {_document_label(kind)} creation for {origin} requires an explicit --number")
|
|
395
|
+
if repo_kind == "extension-pack" and origin == "extension-pack":
|
|
396
|
+
raise ValidationError(f"extension-pack {_document_label(kind)} creation for {origin} requires an explicit --number")
|
|
343
397
|
number = _allocate_number(registry, kind, reserve_range)
|
|
344
398
|
else:
|
|
345
399
|
_ensure_assignable_number(
|
|
346
400
|
registry,
|
|
347
401
|
kind,
|
|
348
402
|
number,
|
|
349
|
-
allow_non_assignable=repo_kind in {"ssot-core", "ssot-origin"
|
|
403
|
+
allow_non_assignable=repo_kind in {"ssot-core", "ssot-origin", "extension-pack"}
|
|
404
|
+
and origin in {"ssot-core", "ssot-origin", "extension-pack"},
|
|
350
405
|
)
|
|
351
406
|
reservation = _reservation_for_number(registry, kind, number)
|
|
352
407
|
if origin in {"ssot-core", "ssot-origin"} and reservation.get("owner") != origin:
|
|
353
408
|
raise ValidationError(
|
|
354
409
|
f"{_document_label(kind)} number {number} must use reservation owned by {origin}; got {reservation.get('owner')}"
|
|
355
410
|
)
|
|
411
|
+
if origin == "extension-pack" and not is_extension_pack_reservation_owner(reservation.get("owner")):
|
|
412
|
+
raise ValidationError(
|
|
413
|
+
f"{_document_label(kind)} number {number} must use reservation owned by {EXTENSION_PACK_RESERVATION_PREFIX}<catalog-id>; got {reservation.get('owner')}"
|
|
414
|
+
)
|
|
356
415
|
|
|
357
416
|
authored_payload = _resolve_authored_payload(kind, title=title, body=body, body_file=body_file, require_one=True)
|
|
358
417
|
authored_payload = _apply_spec_adr_ids(authored_payload, adr_ids) if kind == "spec" else authored_payload
|
|
@@ -596,7 +655,7 @@ def _sync_manifest_document(
|
|
|
596
655
|
raise ValidationError(
|
|
597
656
|
f"Cannot sync {document_id}; number {expected_row['number']} is already used by {row.get('id')}"
|
|
598
657
|
)
|
|
599
|
-
payload =
|
|
658
|
+
payload = read_manifest_document_bytes(kind, manifest_entry)
|
|
600
659
|
target = repo_root / expected_row["path"]
|
|
601
660
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
602
661
|
target.write_bytes(payload)
|
|
@@ -610,7 +669,7 @@ def _sync_manifest_document(
|
|
|
610
669
|
f"Cannot sync {document_id}; field {field_name} was locally modified from {expected_row.get(field_name)} to {current.get(field_name)}"
|
|
611
670
|
)
|
|
612
671
|
|
|
613
|
-
payload =
|
|
672
|
+
payload = read_manifest_document_bytes(kind, manifest_entry)
|
|
614
673
|
target = repo_root / expected_row["path"]
|
|
615
674
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
616
675
|
actual_hash = sha256_normalized_text_path(target) if target.exists() else None
|
|
@@ -641,9 +700,7 @@ def sync_documents_in_memory(registry: dict[str, Any], repo_root: Path, kind: st
|
|
|
641
700
|
lookup = _row_lookup(registry, kind)
|
|
642
701
|
summary: dict[str, list[str]] = {"created": [], "updated": [], "unchanged": []}
|
|
643
702
|
|
|
644
|
-
for manifest_entry in
|
|
645
|
-
if manifest_entry.get("origin") != "ssot-origin":
|
|
646
|
-
raise ValidationError(f"Packaged {kind} manifest entry {manifest_entry.get('id')} must use origin 'ssot-origin'")
|
|
703
|
+
for manifest_entry in _packaged_manifest_entries(kind):
|
|
647
704
|
if not schema_version_meets_minimum(registry.get("schema_version", 0), manifest_entry.get("minimum_schema_version", 0)):
|
|
648
705
|
continue
|
|
649
706
|
outcome, document_id = _sync_manifest_document(registry, repo_root, kind, manifest_entry, lookup)
|
|
@@ -658,7 +715,7 @@ def sync_documents_in_memory(registry: dict[str, Any], repo_root: Path, kind: st
|
|
|
658
715
|
def sync_documents(path: str | Path, kind: str) -> dict[str, Any]:
|
|
659
716
|
registry_path, repo_root, registry = load_registry(path)
|
|
660
717
|
section = section_for_document_kind(kind)
|
|
661
|
-
if _repo_kind(registry) in {"ssot-core", "ssot-origin"}:
|
|
718
|
+
if _repo_kind(registry) in {"ssot-core", "ssot-origin", "extension-pack"}:
|
|
662
719
|
return {
|
|
663
720
|
"passed": True,
|
|
664
721
|
"registry_path": registry_path.as_posix(),
|
|
@@ -3,6 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ssot_registry.guards.claim_closure import evaluate_claim_guard
|
|
6
|
+
from ssot_registry.guards.feature_claims import (
|
|
7
|
+
active_required_feature_claims,
|
|
8
|
+
feature_claim_ceiling_failures,
|
|
9
|
+
)
|
|
6
10
|
from ssot_registry.guards.feature_requirements import evaluate_required_feature_failures
|
|
7
11
|
from ssot_registry.model.enums import CLAIM_TIER_RANK
|
|
8
12
|
from ssot_registry.validators.identity import build_index
|
|
@@ -42,24 +46,23 @@ def evaluate_feature_passing(
|
|
|
42
46
|
|
|
43
47
|
checked_claim_ids: list[str] = []
|
|
44
48
|
satisfying_claim_ids: list[str] = []
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
active_claims = active_required_feature_claims(feature, index)
|
|
50
|
+
for claim in active_claims:
|
|
51
|
+
claim_id = str(claim["id"])
|
|
48
52
|
checked_claim_ids.append(claim_id)
|
|
49
|
-
claim = index["claims"][claim_id]
|
|
50
53
|
guard = evaluate_claim_guard(claim, index, guard_policies)
|
|
51
54
|
if not guard.get("passed"):
|
|
52
55
|
continue
|
|
53
|
-
if required_tier is
|
|
54
|
-
|
|
55
|
-
satisfying_claim_ids.append(claim_id)
|
|
56
|
+
if claim.get("tier") != "T0" and (required_tier is None or CLAIM_TIER_RANK[claim["tier"]] >= CLAIM_TIER_RANK[required_tier]):
|
|
57
|
+
satisfying_claim_ids.append(claim_id)
|
|
56
58
|
|
|
57
|
-
checks["claim_target_met"] = bool(
|
|
59
|
+
checks["claim_target_met"] = bool(active_claims) and len(satisfying_claim_ids) == len(active_claims)
|
|
58
60
|
if not checks["claim_target_met"]:
|
|
59
61
|
if required_tier is None:
|
|
60
62
|
failures.append(f"Feature {feature_id} has no effective required claim tier")
|
|
61
63
|
else:
|
|
62
|
-
failures.append(f"Feature {feature_id}
|
|
64
|
+
failures.append(f"Feature {feature_id} does not have all active required claims satisfying tier {required_tier}")
|
|
65
|
+
failures.extend(failure.removeprefix(f"features.{feature_id} ") for failure in feature_claim_ceiling_failures(feature, index))
|
|
63
66
|
|
|
64
67
|
return {
|
|
65
68
|
"feature_id": feature_id,
|
|
@@ -5,6 +5,11 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from ssot_registry.guards.claim_closure import evaluate_claim_guard
|
|
8
|
+
from ssot_registry.guards.feature_claims import (
|
|
9
|
+
active_required_feature_claims,
|
|
10
|
+
claim_satisfies_feature_implementation,
|
|
11
|
+
feature_claim_ceiling_failures,
|
|
12
|
+
)
|
|
8
13
|
from ssot_registry.model.enums import CLAIM_STATUS_RANK, CLAIM_TIER_RANK
|
|
9
14
|
from ssot_registry.validators.identity import build_index
|
|
10
15
|
|
|
@@ -184,9 +189,7 @@ def _sync_claims(registry: dict[str, Any]) -> list[dict[str, object]]:
|
|
|
184
189
|
|
|
185
190
|
|
|
186
191
|
def _claim_satisfies_feature(claim: dict[str, Any], required_tier: str | None) -> bool:
|
|
187
|
-
|
|
188
|
-
return False
|
|
189
|
-
return CLAIM_STATUS_RANK.get(claim.get("status"), -999) >= CLAIM_STATUS_RANK["evidenced"]
|
|
192
|
+
return claim_satisfies_feature_implementation(claim, required_tier)
|
|
190
193
|
|
|
191
194
|
|
|
192
195
|
def _sync_features_once(registry: dict[str, Any]) -> list[dict[str, object]]:
|
|
@@ -203,25 +206,27 @@ def _sync_features_once(registry: dict[str, Any]) -> list[dict[str, object]]:
|
|
|
203
206
|
else:
|
|
204
207
|
linked_tests = [index["tests"][test_id] for test_id in feature.get("test_ids", []) if test_id in index["tests"]]
|
|
205
208
|
linked_claims = [index["claims"][claim_id] for claim_id in feature.get("claim_ids", []) if claim_id in index["claims"]]
|
|
209
|
+
active_claims = active_required_feature_claims(feature, index)
|
|
206
210
|
required_features = [index["features"][required_id] for required_id in feature.get("requires", []) if required_id in index["features"]]
|
|
207
211
|
required_tier = feature.get("plan", {}).get("target_claim_tier")
|
|
208
212
|
tests_pass = bool(linked_tests) and all(test.get("status") == "passing" for test in linked_tests)
|
|
209
|
-
claims_pass =
|
|
213
|
+
claims_pass = bool(active_claims) and all(_claim_satisfies_feature(claim, required_tier) for claim in active_claims)
|
|
210
214
|
requirements_pass = all(required.get("implementation_status") == "implemented" for required in required_features)
|
|
211
215
|
only_planned_support = (
|
|
212
216
|
bool(linked_tests or linked_claims)
|
|
213
217
|
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
|
|
218
|
+
and all(CLAIM_STATUS_RANK.get(claim.get("status"), -999) <= CLAIM_STATUS_RANK["proposed"] for claim in active_claims)
|
|
215
219
|
)
|
|
216
220
|
if tests_pass and claims_pass and requirements_pass:
|
|
217
221
|
status = "implemented"
|
|
218
|
-
reason = "feature has passing tests,
|
|
222
|
+
reason = "feature has passing tests, all active required claims satisfy implementation, and implemented requirements"
|
|
219
223
|
elif only_planned_support:
|
|
220
224
|
status = "absent"
|
|
221
225
|
reason = "feature has only planned verification support"
|
|
222
226
|
elif linked_tests or linked_claims or required_features:
|
|
223
227
|
status = "partial"
|
|
224
|
-
|
|
228
|
+
ceiling_failures = feature_claim_ceiling_failures(feature, index)
|
|
229
|
+
reason = "; ".join(ceiling_failures) if ceiling_failures else "feature has linked support but does not yet satisfy implementation criteria"
|
|
225
230
|
else:
|
|
226
231
|
status = "absent"
|
|
227
232
|
reason = "feature has no linked implementation support"
|
|
@@ -37,6 +37,7 @@ LEGACY_V3_VERSION = "0.1.2"
|
|
|
37
37
|
SSOT_ORIGIN_V8_OFFSET = 599
|
|
38
38
|
SCHEMA_V0_1_0 = "0.1.0"
|
|
39
39
|
SCHEMA_V0_2_0 = "0.2.0"
|
|
40
|
+
SCHEMA_V0_3_0 = "0.3.0"
|
|
40
41
|
MIGRATION_RELEASE_WINDOWS = {
|
|
41
42
|
(3, 4): "0.1.x->0.2.1",
|
|
42
43
|
(4, 5): "0.2.1->0.2.2",
|
|
@@ -47,7 +48,8 @@ MIGRATION_RELEASE_WINDOWS = {
|
|
|
47
48
|
(9, 10): "0.2.6->0.2.7",
|
|
48
49
|
(10, SCHEMA_V0_1_0): "0.2.7->0.2.7",
|
|
49
50
|
(SCHEMA_V0_1_0, SCHEMA_V0_2_0): "0.2.10->0.2.10",
|
|
50
|
-
(SCHEMA_V0_2_0,
|
|
51
|
+
(SCHEMA_V0_2_0, SCHEMA_V0_3_0): "0.2.10->0.3.0",
|
|
52
|
+
(SCHEMA_V0_3_0, SCHEMA_VERSION): "0.2.10->0.4.0",
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
MIGRATION_PATHS = (
|
|
@@ -60,7 +62,8 @@ MIGRATION_PATHS = (
|
|
|
60
62
|
(9, 10, "migrate_v9_to_v10"),
|
|
61
63
|
(10, SCHEMA_V0_1_0, "migrate_v10_to_v0_1_0"),
|
|
62
64
|
(SCHEMA_V0_1_0, SCHEMA_V0_2_0, "migrate_v0_1_0_to_v0_2_0"),
|
|
63
|
-
(SCHEMA_V0_2_0,
|
|
65
|
+
(SCHEMA_V0_2_0, SCHEMA_V0_3_0, "migrate_v0_2_0_to_v0_3_0"),
|
|
66
|
+
(SCHEMA_V0_3_0, SCHEMA_VERSION, "migrate_v0_3_0_to_v0_4_0"),
|
|
64
67
|
)
|
|
65
68
|
|
|
66
69
|
|
|
@@ -527,11 +530,24 @@ def migrate_v0_2_0_to_v0_3_0(
|
|
|
527
530
|
) -> dict[str, Any]:
|
|
528
531
|
_ = repo_root, previous_version, target_version
|
|
529
532
|
migrated = deepcopy(registry)
|
|
530
|
-
migrated["schema_version"] =
|
|
533
|
+
migrated["schema_version"] = SCHEMA_V0_3_0
|
|
531
534
|
_seed_release_boundary_ids(migrated)
|
|
532
535
|
return migrated
|
|
533
536
|
|
|
534
537
|
|
|
538
|
+
def migrate_v0_3_0_to_v0_4_0(
|
|
539
|
+
registry: dict[str, Any],
|
|
540
|
+
repo_root: Path,
|
|
541
|
+
*,
|
|
542
|
+
previous_version: str,
|
|
543
|
+
target_version: str,
|
|
544
|
+
) -> dict[str, Any]:
|
|
545
|
+
_ = repo_root, previous_version, target_version
|
|
546
|
+
migrated = deepcopy(registry)
|
|
547
|
+
migrated["schema_version"] = SCHEMA_VERSION
|
|
548
|
+
return migrated
|
|
549
|
+
|
|
550
|
+
|
|
535
551
|
def target_version_from_registry(registry: dict[str, Any]) -> str:
|
|
536
552
|
tooling = registry.get("tooling")
|
|
537
553
|
if isinstance(tooling, dict):
|
|
@@ -668,11 +684,21 @@ def upgrade_registry(
|
|
|
668
684
|
target_version=target_version,
|
|
669
685
|
)
|
|
670
686
|
schema_migrations.append("migrate_v0_2_0_to_v0_3_0")
|
|
671
|
-
migrations.append(_migration_window_label(SCHEMA_V0_2_0,
|
|
687
|
+
migrations.append(_migration_window_label(SCHEMA_V0_2_0, SCHEMA_V0_3_0))
|
|
688
|
+
source_schema = SCHEMA_V0_3_0
|
|
689
|
+
if source_schema == SCHEMA_V0_3_0:
|
|
690
|
+
working = migrate_v0_3_0_to_v0_4_0(
|
|
691
|
+
working,
|
|
692
|
+
repo_root,
|
|
693
|
+
previous_version=source_tooling_version,
|
|
694
|
+
target_version=target_version,
|
|
695
|
+
)
|
|
696
|
+
schema_migrations.append("migrate_v0_3_0_to_v0_4_0")
|
|
697
|
+
migrations.append(_migration_window_label(SCHEMA_V0_3_0, SCHEMA_VERSION))
|
|
672
698
|
source_schema = SCHEMA_VERSION
|
|
673
699
|
elif source_schema != SCHEMA_VERSION:
|
|
674
700
|
raise RegistryError(
|
|
675
|
-
f"Unsupported registry schema_version {source_schema}; expected 3, 4, 5, 6, 7, 8, 9, 10, 0.1.0, 0.2.0 or {SCHEMA_VERSION}"
|
|
701
|
+
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 or {SCHEMA_VERSION}"
|
|
676
702
|
)
|
|
677
703
|
|
|
678
704
|
normalized_current = _normalize_current_registry(working)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ssot_registry.model.enums import CLAIM_STATUS_RANK, CLAIM_TIER_RANK
|
|
6
|
+
|
|
7
|
+
INACTIVE_CLAIM_STATUSES = {"retired"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def active_required_feature_claims(
|
|
11
|
+
feature: dict[str, Any],
|
|
12
|
+
index: dict[str, dict[str, dict[str, object]]],
|
|
13
|
+
) -> list[dict[str, object]]:
|
|
14
|
+
return [
|
|
15
|
+
index["claims"][claim_id]
|
|
16
|
+
for claim_id in feature.get("claim_ids", [])
|
|
17
|
+
if claim_id in index["claims"] and index["claims"][claim_id].get("status") not in INACTIVE_CLAIM_STATUSES
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def claim_satisfies_feature_implementation(
|
|
22
|
+
claim: dict[str, object],
|
|
23
|
+
required_tier: str | None,
|
|
24
|
+
) -> bool:
|
|
25
|
+
if claim.get("tier") == "T0":
|
|
26
|
+
return False
|
|
27
|
+
if required_tier is not None and CLAIM_TIER_RANK[claim["tier"]] < CLAIM_TIER_RANK[required_tier]:
|
|
28
|
+
return False
|
|
29
|
+
return CLAIM_STATUS_RANK.get(claim.get("status"), -999) >= CLAIM_STATUS_RANK["evidenced"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def feature_claim_ceiling_failures(
|
|
33
|
+
feature: dict[str, Any],
|
|
34
|
+
index: dict[str, dict[str, dict[str, object]]],
|
|
35
|
+
*,
|
|
36
|
+
require_evidenced_status: bool = True,
|
|
37
|
+
) -> list[str]:
|
|
38
|
+
feature_id = str(feature["id"])
|
|
39
|
+
required_tier = feature.get("plan", {}).get("target_claim_tier")
|
|
40
|
+
failures: list[str] = []
|
|
41
|
+
for claim in active_required_feature_claims(feature, index):
|
|
42
|
+
claim_id = str(claim["id"])
|
|
43
|
+
claim_tier = str(claim["tier"])
|
|
44
|
+
claim_status = str(claim.get("status"))
|
|
45
|
+
if claim_tier == "T0":
|
|
46
|
+
failures.append(f"features.{feature_id} is capped below implemented by active T0 claim {claim_id}")
|
|
47
|
+
continue
|
|
48
|
+
if required_tier is not None and CLAIM_TIER_RANK[claim_tier] < CLAIM_TIER_RANK[required_tier]:
|
|
49
|
+
failures.append(
|
|
50
|
+
f"features.{feature_id} is capped below implemented by claim {claim_id} tier {claim_tier} below target {required_tier}"
|
|
51
|
+
)
|
|
52
|
+
continue
|
|
53
|
+
if require_evidenced_status and CLAIM_STATUS_RANK.get(claim_status, -999) < CLAIM_STATUS_RANK["evidenced"]:
|
|
54
|
+
failures.append(
|
|
55
|
+
f"features.{feature_id} is capped below implemented by claim {claim_id} status {claim_status} below evidenced"
|
|
56
|
+
)
|
|
57
|
+
return failures
|
|
@@ -12,10 +12,14 @@ from .document import (
|
|
|
12
12
|
document_path_has_supported_suffix,
|
|
13
13
|
document_path_variants,
|
|
14
14
|
default_document_id_reservations,
|
|
15
|
+
extension_pack_reservation_owner,
|
|
15
16
|
format_document_number,
|
|
17
|
+
is_extension_pack_reservation_owner,
|
|
16
18
|
load_document_manifest,
|
|
17
19
|
normalize_document_id,
|
|
18
20
|
parse_document_filename,
|
|
21
|
+
read_manifest_document_bytes,
|
|
22
|
+
read_manifest_document_text,
|
|
19
23
|
read_packaged_document_bytes,
|
|
20
24
|
read_packaged_document_text,
|
|
21
25
|
)
|
|
@@ -39,15 +43,19 @@ __all__ = [
|
|
|
39
43
|
"build_minimal_registry",
|
|
40
44
|
"count_entities",
|
|
41
45
|
"default_document_id_reservations",
|
|
46
|
+
"extension_pack_reservation_owner",
|
|
42
47
|
"default_guard_policies",
|
|
43
48
|
"default_paths",
|
|
44
49
|
"filesystem_safe_id",
|
|
45
50
|
"format_document_number",
|
|
51
|
+
"is_extension_pack_reservation_owner",
|
|
46
52
|
"is_normalized_id",
|
|
47
53
|
"load_document_manifest",
|
|
48
54
|
"normalize_document_id",
|
|
49
55
|
"parse_document_filename",
|
|
50
56
|
"ProfileRow",
|
|
57
|
+
"read_manifest_document_bytes",
|
|
58
|
+
"read_manifest_document_text",
|
|
51
59
|
"read_packaged_document_bytes",
|
|
52
60
|
"read_packaged_document_text",
|
|
53
61
|
]
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, TypedDict
|
|
5
|
+
from typing import Any, Callable, TypedDict
|
|
6
6
|
|
|
7
7
|
from ssot_contracts import load_document_manifest as contracts_load_document_manifest
|
|
8
8
|
from ssot_contracts import read_packaged_document_bytes as contracts_read_packaged_document_bytes
|
|
@@ -17,7 +17,8 @@ DOCUMENT_FILENAME_PREFIXES = CONTRACT_DATA["document_contract"]["filename_prefix
|
|
|
17
17
|
DOCUMENT_PATH_KEYS = CONTRACT_DATA["document_contract"]["path_keys"]
|
|
18
18
|
DOCUMENT_RESERVATION_KEYS = CONTRACT_DATA["document_contract"]["reservation_keys"]
|
|
19
19
|
DOCUMENT_FILE_SUFFIXES = (".json", ".yaml")
|
|
20
|
-
DOCUMENT_ORIGINS = {"ssot-core", "ssot-origin", "repo-local"}
|
|
20
|
+
DOCUMENT_ORIGINS = {"ssot-core", "ssot-origin", "repo-local", "extension-pack"}
|
|
21
|
+
EXTENSION_PACK_RESERVATION_PREFIX = "extension-pack:"
|
|
21
22
|
DOCUMENT_STATUSES = tuple(CONTRACT_DATA["choice_sets"]["document_statuses"])
|
|
22
23
|
CREATE_ALLOWED_STATUSES = ("draft", "in_review", "accepted", "rejected", "withdrawn")
|
|
23
24
|
TERMINAL_STATUSES = ("rejected", "withdrawn", "superseded", "retired")
|
|
@@ -65,6 +66,14 @@ class SpecRow(TypedDict, total=False):
|
|
|
65
66
|
status_notes: list[StatusNote]
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
class DocumentCatalogProvider(TypedDict):
|
|
70
|
+
catalog_id: str
|
|
71
|
+
trusted_by_default: bool
|
|
72
|
+
load_manifest: Callable[[str], list[dict[str, Any]]]
|
|
73
|
+
read_text: Callable[[str, str], str]
|
|
74
|
+
read_bytes: Callable[[str, str], bytes]
|
|
75
|
+
|
|
76
|
+
|
|
68
77
|
def format_document_number(number: int) -> str:
|
|
69
78
|
return f"{number:04d}"
|
|
70
79
|
|
|
@@ -114,8 +123,54 @@ def default_document_id_reservations() -> dict[str, list[dict[str, Any]]]:
|
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
_DOCUMENT_CATALOG_PROVIDERS: list[DocumentCatalogProvider] = [
|
|
127
|
+
{
|
|
128
|
+
"catalog_id": "ssot-origin",
|
|
129
|
+
"trusted_by_default": True,
|
|
130
|
+
"load_manifest": contracts_load_document_manifest,
|
|
131
|
+
"read_text": contracts_read_packaged_document_text,
|
|
132
|
+
"read_bytes": contracts_read_packaged_document_bytes,
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def extension_pack_reservation_owner(catalog_id: str) -> str:
|
|
138
|
+
return f"{EXTENSION_PACK_RESERVATION_PREFIX}{catalog_id}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def is_extension_pack_reservation_owner(owner: object) -> bool:
|
|
142
|
+
return isinstance(owner, str) and owner.startswith(EXTENSION_PACK_RESERVATION_PREFIX) and len(owner) > len(
|
|
143
|
+
EXTENSION_PACK_RESERVATION_PREFIX
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_document_manifest(kind: str, *, include_untrusted: bool = False) -> list[dict[str, Any]]:
|
|
148
|
+
entries: list[dict[str, Any]] = []
|
|
149
|
+
for provider in _DOCUMENT_CATALOG_PROVIDERS:
|
|
150
|
+
if not include_untrusted and not provider["trusted_by_default"]:
|
|
151
|
+
continue
|
|
152
|
+
for raw_entry in provider["load_manifest"](kind):
|
|
153
|
+
entry = dict(raw_entry)
|
|
154
|
+
entry.setdefault("catalog_id", provider["catalog_id"])
|
|
155
|
+
entry.setdefault("trusted", provider["trusted_by_default"])
|
|
156
|
+
entries.append(entry)
|
|
157
|
+
return entries
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def read_manifest_document_text(kind: str, manifest_entry: dict[str, Any]) -> str:
|
|
161
|
+
catalog_id = str(manifest_entry.get("catalog_id", "ssot-origin"))
|
|
162
|
+
for provider in _DOCUMENT_CATALOG_PROVIDERS:
|
|
163
|
+
if provider["catalog_id"] == catalog_id:
|
|
164
|
+
return provider["read_text"](kind, str(manifest_entry["filename"]))
|
|
165
|
+
raise ValueError(f"Unknown document catalog provider: {catalog_id}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def read_manifest_document_bytes(kind: str, manifest_entry: dict[str, Any]) -> bytes:
|
|
169
|
+
catalog_id = str(manifest_entry.get("catalog_id", "ssot-origin"))
|
|
170
|
+
for provider in _DOCUMENT_CATALOG_PROVIDERS:
|
|
171
|
+
if provider["catalog_id"] == catalog_id:
|
|
172
|
+
return provider["read_bytes"](kind, str(manifest_entry["filename"]))
|
|
173
|
+
raise ValueError(f"Unknown document catalog provider: {catalog_id}")
|
|
119
174
|
|
|
120
175
|
|
|
121
176
|
def read_packaged_document_text(kind: str, filename: str) -> str:
|