ssot-conformance 0.2.15__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_conformance-0.2.15/PKG-INFO +89 -0
- ssot_conformance-0.2.15/README.md +58 -0
- ssot_conformance-0.2.15/pyproject.toml +49 -0
- ssot_conformance-0.2.15/setup.cfg +4 -0
- ssot_conformance-0.2.15/src/ssot_conformance/__init__.py +27 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/__init__.py +1 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/test_boundary_release_contract.py +21 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/test_document_contract.py +16 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/test_feature_spec_contract.py +13 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/test_id_contract.py +11 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/test_proof_chain_contract.py +23 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/test_registry_contract.py +14 -0
- ssot_conformance-0.2.15/src/ssot_conformance/cases/test_spec_adr_contract.py +11 -0
- ssot_conformance-0.2.15/src/ssot_conformance/catalog.py +346 -0
- ssot_conformance-0.2.15/src/ssot_conformance/discovery.py +25 -0
- ssot_conformance-0.2.15/src/ssot_conformance/evidence.py +34 -0
- ssot_conformance-0.2.15/src/ssot_conformance/plugin.py +92 -0
- ssot_conformance-0.2.15/src/ssot_conformance/runner.py +138 -0
- ssot_conformance-0.2.15/src/ssot_conformance/scaffold.py +150 -0
- ssot_conformance-0.2.15/src/ssot_conformance.egg-info/PKG-INFO +89 -0
- ssot_conformance-0.2.15/src/ssot_conformance.egg-info/SOURCES.txt +23 -0
- ssot_conformance-0.2.15/src/ssot_conformance.egg-info/dependency_links.txt +1 -0
- ssot_conformance-0.2.15/src/ssot_conformance.egg-info/entry_points.txt +2 -0
- ssot_conformance-0.2.15/src/ssot_conformance.egg-info/requires.txt +5 -0
- ssot_conformance-0.2.15/src/ssot_conformance.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ssot-conformance
|
|
3
|
+
Version: 0.2.15
|
|
4
|
+
Summary: Reusable SSOT conformance harness, pytest plugin, and scaffold helpers.
|
|
5
|
+
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/groupsum/ssot-registry/tree/main/pkgs/ssot-conformance
|
|
8
|
+
Project-URL: Repository, https://github.com/groupsum/ssot-registry/tree/main/pkgs/ssot-conformance
|
|
9
|
+
Project-URL: Issues, https://github.com/groupsum/ssot-registry/issues
|
|
10
|
+
Keywords: ssot,conformance,pytest,governance,validation,scaffold
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Plugins
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: <3.14,>=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: ssot-core==0.2.15
|
|
29
|
+
Requires-Dist: ssot-contracts==0.2.15
|
|
30
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
<h1>🔷 ssot-conformance</h1>
|
|
34
|
+
<p><strong>Reusable SSOT conformance harness for downstream repositories.</strong></p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div align="center">
|
|
38
|
+
<a href="https://pypi.org/project/ssot-conformance/"><img src="https://img.shields.io/pypi/v/ssot-conformance?label=PyPI%20version" alt="PyPI version" /></a>
|
|
39
|
+
<a href="https://pypi.org/project/ssot-conformance/"><img src="https://img.shields.io/pypi/pyversions/ssot-conformance?label=Python" alt="Supported Python versions" /></a>
|
|
40
|
+
<a href="https://pepy.tech/project/ssot-conformance"><img src="https://static.pepy.tech/badge/ssot-conformance" alt="Downloads" /></a>
|
|
41
|
+
<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="Repository hits" /></a>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
Reusable SSOT conformance harness for downstream repositories.
|
|
45
|
+
|
|
46
|
+
This package provides:
|
|
47
|
+
|
|
48
|
+
- portable conformance case families grouped by registry, document, id, SPEC-to-ADR, feature-to-SPEC, proof-chain, and boundary/release concerns
|
|
49
|
+
- a `pytest` plugin entry point for repo-root selection, case-family filtering, and evidence-output emission
|
|
50
|
+
- scaffold helpers that can compute and optionally create missing conformance SSOT rows
|
|
51
|
+
- machine-readable evidence output suitable for later SSOT evidence ingestion and status synchronization
|
|
52
|
+
|
|
53
|
+
The package is intentionally repo-agnostic. It evaluates a target repository through `.ssot` artifacts and registry semantics rather than assuming a specific implementation language for the target system.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install ssot-conformance
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For local development from this repository:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e pkgs/ssot-conformance
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Execution model
|
|
68
|
+
|
|
69
|
+
Packaged `ssot-core` conformance remains pytest-based, but the operator-facing execution model is registry-driven:
|
|
70
|
+
|
|
71
|
+
- packaged conformance rows store executable `tests[].execution` metadata
|
|
72
|
+
- `ssot conformance run` resolves governed conformance tests and runs those stored commands
|
|
73
|
+
- downstream repos can use the same `tests[].execution` contract for arbitrary command-backed suites
|
|
74
|
+
|
|
75
|
+
For direct SSOT entity execution, prefer:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
ssot test run . --id tst:pytest.conformance.registry-contract
|
|
79
|
+
ssot spec run-tests . --id spc:0525
|
|
80
|
+
ssot boundary run-tests . --id bnd:full-cert
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Package relationships
|
|
84
|
+
|
|
85
|
+
- Package type: reusable conformance and pytest plugin package
|
|
86
|
+
- Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
|
|
87
|
+
- Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/) and downstream repositories that run packaged SSOT conformance checks
|
|
88
|
+
|
|
89
|
+
If you need the CLI wrapper for these checks, install [ssot-cli](https://pypi.org/project/ssot-cli/). If you need to embed or run the reusable conformance cases directly, this is the package to install.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>🔷 ssot-conformance</h1>
|
|
3
|
+
<p><strong>Reusable SSOT conformance harness for downstream repositories.</strong></p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div align="center">
|
|
7
|
+
<a href="https://pypi.org/project/ssot-conformance/"><img src="https://img.shields.io/pypi/v/ssot-conformance?label=PyPI%20version" alt="PyPI version" /></a>
|
|
8
|
+
<a href="https://pypi.org/project/ssot-conformance/"><img src="https://img.shields.io/pypi/pyversions/ssot-conformance?label=Python" alt="Supported Python versions" /></a>
|
|
9
|
+
<a href="https://pepy.tech/project/ssot-conformance"><img src="https://static.pepy.tech/badge/ssot-conformance" alt="Downloads" /></a>
|
|
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="Repository hits" /></a>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
Reusable SSOT conformance harness for downstream repositories.
|
|
14
|
+
|
|
15
|
+
This package provides:
|
|
16
|
+
|
|
17
|
+
- portable conformance case families grouped by registry, document, id, SPEC-to-ADR, feature-to-SPEC, proof-chain, and boundary/release concerns
|
|
18
|
+
- a `pytest` plugin entry point for repo-root selection, case-family filtering, and evidence-output emission
|
|
19
|
+
- scaffold helpers that can compute and optionally create missing conformance SSOT rows
|
|
20
|
+
- machine-readable evidence output suitable for later SSOT evidence ingestion and status synchronization
|
|
21
|
+
|
|
22
|
+
The package is intentionally repo-agnostic. It evaluates a target repository through `.ssot` artifacts and registry semantics rather than assuming a specific implementation language for the target system.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python -m pip install ssot-conformance
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For local development from this repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m pip install -e pkgs/ssot-conformance
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Execution model
|
|
37
|
+
|
|
38
|
+
Packaged `ssot-core` conformance remains pytest-based, but the operator-facing execution model is registry-driven:
|
|
39
|
+
|
|
40
|
+
- packaged conformance rows store executable `tests[].execution` metadata
|
|
41
|
+
- `ssot conformance run` resolves governed conformance tests and runs those stored commands
|
|
42
|
+
- downstream repos can use the same `tests[].execution` contract for arbitrary command-backed suites
|
|
43
|
+
|
|
44
|
+
For direct SSOT entity execution, prefer:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
ssot test run . --id tst:pytest.conformance.registry-contract
|
|
48
|
+
ssot spec run-tests . --id spc:0525
|
|
49
|
+
ssot boundary run-tests . --id bnd:full-cert
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Package relationships
|
|
53
|
+
|
|
54
|
+
- Package type: reusable conformance and pytest plugin package
|
|
55
|
+
- Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
|
|
56
|
+
- Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/) and downstream repositories that run packaged SSOT conformance checks
|
|
57
|
+
|
|
58
|
+
If you need the CLI wrapper for these checks, install [ssot-cli](https://pypi.org/project/ssot-cli/). If you need to embed or run the reusable conformance cases directly, this is the package to install.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ssot-conformance"
|
|
7
|
+
version = "0.2.15"
|
|
8
|
+
description = "Reusable SSOT conformance harness, pytest plugin, and scaffold helpers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10,<3.14"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"ssot-core==0.2.15",
|
|
15
|
+
"ssot-contracts==0.2.15",
|
|
16
|
+
"tomli>=2.0.1; python_version < '3.11'",
|
|
17
|
+
]
|
|
18
|
+
keywords = ["ssot", "conformance", "pytest", "governance", "validation", "scaffold"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Environment :: Plugins",
|
|
22
|
+
"Framework :: Pytest",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Intended Audience :: Information Technology",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
33
|
+
"Topic :: Software Development :: Testing",
|
|
34
|
+
"Topic :: Utilities",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/groupsum/ssot-registry/tree/main/pkgs/ssot-conformance"
|
|
39
|
+
Repository = "https://github.com/groupsum/ssot-registry/tree/main/pkgs/ssot-conformance"
|
|
40
|
+
Issues = "https://github.com/groupsum/ssot-registry/issues"
|
|
41
|
+
|
|
42
|
+
[project.entry-points.pytest11]
|
|
43
|
+
ssot-conformance = "ssot_conformance.plugin"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
package-dir = {"" = "src"}
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .catalog import CATALOG_VERSION, build_catalog_slice, list_profiles, resolve_selected_families
|
|
2
|
+
from .discovery import discover_cases
|
|
3
|
+
from .evidence import build_evidence_output, write_evidence_output
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def plan_scaffold(*args, **kwargs):
|
|
7
|
+
from .scaffold import plan_scaffold as _plan_scaffold
|
|
8
|
+
|
|
9
|
+
return _plan_scaffold(*args, **kwargs)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def apply_scaffold(*args, **kwargs):
|
|
13
|
+
from .scaffold import apply_scaffold as _apply_scaffold
|
|
14
|
+
|
|
15
|
+
return _apply_scaffold(*args, **kwargs)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CATALOG_VERSION",
|
|
19
|
+
"build_catalog_slice",
|
|
20
|
+
"list_profiles",
|
|
21
|
+
"resolve_selected_families",
|
|
22
|
+
"discover_cases",
|
|
23
|
+
"build_evidence_output",
|
|
24
|
+
"write_evidence_output",
|
|
25
|
+
"plan_scaffold",
|
|
26
|
+
"apply_scaffold",
|
|
27
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Portable SSOT conformance case families."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
CONFORMANCE_FAMILY = "boundary-release"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_boundary_release_contract(ssot_registry) -> None:
|
|
7
|
+
features = {row["id"] for row in ssot_registry["features"]}
|
|
8
|
+
claims = {row["id"] for row in ssot_registry["claims"]}
|
|
9
|
+
evidence = {row["id"] for row in ssot_registry["evidence"]}
|
|
10
|
+
boundaries = {row["id"]: row for row in ssot_registry["boundaries"]}
|
|
11
|
+
for boundary in boundaries.values():
|
|
12
|
+
for feature_id in boundary.get("feature_ids", []):
|
|
13
|
+
assert feature_id in features, (boundary["id"], feature_id)
|
|
14
|
+
for release in ssot_registry["releases"]:
|
|
15
|
+
assert release["boundary_id"] in boundaries, release["id"]
|
|
16
|
+
for boundary_id in release.get("boundary_ids", [release["boundary_id"]]):
|
|
17
|
+
assert boundary_id in boundaries, (release["id"], boundary_id)
|
|
18
|
+
for claim_id in release.get("claim_ids", []):
|
|
19
|
+
assert claim_id in claims, (release["id"], claim_id)
|
|
20
|
+
for evidence_id in release.get("evidence_ids", []):
|
|
21
|
+
assert evidence_id in evidence, (release["id"], evidence_id)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
CONFORMANCE_FAMILY = "document"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_document_contract(ssot_repo_root, ssot_registry) -> None:
|
|
9
|
+
for section in ("adrs", "specs"):
|
|
10
|
+
for row in ssot_registry[section]:
|
|
11
|
+
path = Path(ssot_repo_root) / row["path"]
|
|
12
|
+
assert path.exists(), row["id"]
|
|
13
|
+
if row["origin"] == "repo-local":
|
|
14
|
+
assert row["number"] >= 1000, row["id"]
|
|
15
|
+
else:
|
|
16
|
+
assert row["number"] < 1000, row["id"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
CONFORMANCE_FAMILY = "feature-spec"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_feature_spec_contract(ssot_registry) -> None:
|
|
7
|
+
spec_ids = {row["id"] for row in ssot_registry["specs"]}
|
|
8
|
+
for feature in ssot_registry["features"]:
|
|
9
|
+
assert isinstance(feature.get("spec_ids", []), list), feature["id"]
|
|
10
|
+
for spec_id in feature.get("spec_ids", []):
|
|
11
|
+
assert spec_id in spec_ids, (feature["id"], spec_id)
|
|
12
|
+
for spec in ssot_registry["specs"]:
|
|
13
|
+
assert "feature_ids" not in spec, spec["id"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ssot_registry.model.ids import is_normalized_id
|
|
4
|
+
|
|
5
|
+
CONFORMANCE_FAMILY = "id"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_id_contract(ssot_registry) -> None:
|
|
9
|
+
for section in ("features", "tests", "claims", "evidence", "issues", "risks", "boundaries", "releases", "adrs", "specs"):
|
|
10
|
+
for row in ssot_registry.get(section, []):
|
|
11
|
+
assert is_normalized_id(row["id"]), f"{section}.{row['id']}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
CONFORMANCE_FAMILY = "proof-chain"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_proof_chain_contract(ssot_registry) -> None:
|
|
7
|
+
claims = {row["id"]: row for row in ssot_registry["claims"]}
|
|
8
|
+
tests = {row["id"]: row for row in ssot_registry["tests"]}
|
|
9
|
+
evidence = {row["id"]: row for row in ssot_registry["evidence"]}
|
|
10
|
+
for feature in ssot_registry["features"]:
|
|
11
|
+
if feature.get("implementation_status") != "implemented":
|
|
12
|
+
continue
|
|
13
|
+
assert feature["claim_ids"], feature["id"]
|
|
14
|
+
assert feature["test_ids"], feature["id"]
|
|
15
|
+
for claim_id in feature["claim_ids"]:
|
|
16
|
+
assert claim_id in claims, (feature["id"], claim_id)
|
|
17
|
+
for test_id in feature["test_ids"]:
|
|
18
|
+
assert test_id in tests, (feature["id"], test_id)
|
|
19
|
+
for claim in ssot_registry["claims"]:
|
|
20
|
+
for test_id in claim.get("test_ids", []):
|
|
21
|
+
assert test_id in tests, (claim["id"], test_id)
|
|
22
|
+
for evidence_id in claim.get("evidence_ids", []):
|
|
23
|
+
assert evidence_id in evidence, (claim["id"], evidence_id)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ssot_contracts.generated.python.enums import SCHEMA_VERSION
|
|
4
|
+
from ssot_registry.api import validate_registry
|
|
5
|
+
|
|
6
|
+
CONFORMANCE_FAMILY = "registry"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_registry_contract(ssot_repo_root, ssot_registry) -> None:
|
|
10
|
+
report = validate_registry(ssot_repo_root)
|
|
11
|
+
assert report["passed"], report
|
|
12
|
+
assert ssot_registry["schema_version"] == SCHEMA_VERSION
|
|
13
|
+
for section in ("repo", "paths", "guard_policies", "features", "tests", "claims", "evidence", "adrs", "specs"):
|
|
14
|
+
assert section in ssot_registry
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
CONFORMANCE_FAMILY = "spec-adr"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_spec_adr_contract(ssot_registry) -> None:
|
|
7
|
+
adr_ids = {row["id"] for row in ssot_registry["adrs"]}
|
|
8
|
+
for row in ssot_registry["specs"]:
|
|
9
|
+
assert isinstance(row.get("adr_ids", []), list), row["id"]
|
|
10
|
+
for adr_id in row.get("adr_ids", []):
|
|
11
|
+
assert adr_id in adr_ids, (row["id"], adr_id)
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
CATALOG_VERSION = "0.2.14"
|
|
8
|
+
|
|
9
|
+
FAMILIES = (
|
|
10
|
+
"package-layout",
|
|
11
|
+
"plugin",
|
|
12
|
+
"registry",
|
|
13
|
+
"document",
|
|
14
|
+
"id",
|
|
15
|
+
"spec-adr",
|
|
16
|
+
"feature-spec",
|
|
17
|
+
"proof-chain",
|
|
18
|
+
"boundary-release",
|
|
19
|
+
"scaffold",
|
|
20
|
+
"evidence-output",
|
|
21
|
+
"cli",
|
|
22
|
+
"command-runner",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
PROFILE_DEFINITIONS = {
|
|
26
|
+
"all": list(FAMILIES),
|
|
27
|
+
"core": list(FAMILIES),
|
|
28
|
+
**{family: [family] for family in FAMILIES},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_FEATURES = {
|
|
32
|
+
"package-layout": [
|
|
33
|
+
("feat:conformance.package-layout", "SSOT conformance package layout", "Provide the ssot-conformance workspace package structure and package metadata.", ["spc:0524"], ["clm:conformance.package-layout.t2"], ["tst:pytest.conformance.package-layout"]),
|
|
34
|
+
],
|
|
35
|
+
"plugin": [
|
|
36
|
+
("feat:conformance.pytest-plugin", "SSOT conformance pytest plugin", "Expose pytest integration for running portable SSOT conformance cases against a target repository.", ["spc:0524"], ["clm:conformance.pytest-plugin.t2"], ["tst:pytest.conformance.pytest-plugin"]),
|
|
37
|
+
("feat:conformance.case-discovery", "SSOT conformance case discovery", "Discover selected conformance profiles and case families before execution or scaffolding.", ["spc:0524", "spc:0525"], ["clm:conformance.case-discovery.t2"], ["tst:pytest.conformance.pytest-plugin"]),
|
|
38
|
+
],
|
|
39
|
+
"registry": [
|
|
40
|
+
("feat:conformance.registry-contract", "Registry conformance contract cases", "Validate registry existence, schema version, required sections, packaged schema conformance, and canonical JSON expectations.", ["spc:0525"], ["clm:conformance.registry-contract.t2"], ["tst:pytest.conformance.registry-contract"]),
|
|
41
|
+
],
|
|
42
|
+
"document": [
|
|
43
|
+
("feat:conformance.document-contract", "Document conformance contract cases", "Validate ADR and SPEC document rows, JSON files, hashes, origins, and numbering ranges.", ["spc:0525"], ["clm:conformance.document-contract.t2"], ["tst:pytest.conformance.document-contract"]),
|
|
44
|
+
],
|
|
45
|
+
"id": [
|
|
46
|
+
("feat:conformance.id-contract", "Normalized ID conformance contract cases", "Validate normalized entity IDs, reference IDs, and maximum identifier length policy.", ["spc:0525"], ["clm:conformance.id-contract.t2"], ["tst:pytest.conformance.id-contract"]),
|
|
47
|
+
],
|
|
48
|
+
"spec-adr": [
|
|
49
|
+
("feat:conformance.spec-adr-contract", "SPEC-to-ADR conformance contract cases", "Validate typed SPEC adr_ids links and derived SPEC-to-ADR graph visibility.", ["spc:0525"], ["clm:conformance.spec-adr-contract.t2"], ["tst:pytest.conformance.spec-adr-contract"]),
|
|
50
|
+
],
|
|
51
|
+
"feature-spec": [
|
|
52
|
+
("feat:conformance.feature-spec-contract", "Feature-to-SPEC conformance contract cases", "Validate feature-owned spec_ids links and confirm SPEC entities do not own feature_ids.", ["spc:0525", "spc:0526"], ["clm:conformance.feature-spec-contract.t2"], ["tst:pytest.conformance.feature-spec-contract"]),
|
|
53
|
+
],
|
|
54
|
+
"proof-chain": [
|
|
55
|
+
("feat:conformance.proof-chain-contract", "Proof-chain conformance contract cases", "Validate feature, claim, test, and evidence readiness relationships for targeted conformance rows.", ["spc:0525"], ["clm:conformance.proof-chain-contract.t2"], ["tst:pytest.conformance.proof-chain-contract"]),
|
|
56
|
+
],
|
|
57
|
+
"boundary-release": [
|
|
58
|
+
("feat:conformance.boundary-release-contract", "Boundary and release conformance contract cases", "Validate frozen boundary and release closure references used for conformance certification.", ["spc:0525"], ["clm:conformance.boundary-release-contract.t2"], ["tst:pytest.conformance.boundary-release-contract"]),
|
|
59
|
+
],
|
|
60
|
+
"scaffold": [
|
|
61
|
+
("feat:conformance.scaffold-dry-run", "Conformance scaffold dry-run planning", "Compute missing conformance feature and test rows without mutating the target registry.", ["spc:0526"], ["clm:conformance.scaffold-dry-run.t2"], ["tst:pytest.conformance.scaffold"]),
|
|
62
|
+
("feat:conformance.scaffold-apply", "Conformance scaffold apply mode", "Create missing conformance feature and test rows through SSOT APIs while preserving validation.", ["spc:0526"], ["clm:conformance.scaffold-apply.t2"], ["tst:pytest.conformance.scaffold"]),
|
|
63
|
+
("feat:conformance.scaffold-idempotency", "Conformance scaffold idempotency", "Ensure repeated scaffold runs do not duplicate rows or churn stable identifiers.", ["spc:0526"], ["clm:conformance.scaffold-idempotency.t2"], ["tst:pytest.conformance.scaffold"]),
|
|
64
|
+
],
|
|
65
|
+
"evidence-output": [
|
|
66
|
+
("feat:conformance.evidence-output", "Conformance evidence output", "Emit machine-readable conformance evidence suitable for registry status synchronization.", ["spc:0526"], ["clm:conformance.evidence-output.t2"], ["tst:pytest.conformance.evidence-output"]),
|
|
67
|
+
],
|
|
68
|
+
"cli": [
|
|
69
|
+
("feat:cli.conformance-surface", "CLI conformance command surface", "Expose ssot conformance commands for profile listing, discovery, scaffolding, execution, and evidence handling.", ["spc:0524", "spc:0526"], ["clm:cli.conformance-surface.t2"], ["tst:pytest.cli.conformance-surface"]),
|
|
70
|
+
],
|
|
71
|
+
"command-runner": [
|
|
72
|
+
("feat:conformance.command-runner", "Registry-driven conformance command runner", "Execute governed conformance rows through registry-owned command metadata instead of ad hoc runner flags.", ["spc:0524", "spc:0526"], ["clm:conformance.command-runner.t2"], ["tst:command.conformance.command-runner"]),
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_CLAIMS = {
|
|
77
|
+
"package-layout": [
|
|
78
|
+
("clm:conformance.package-layout.t2", "Conformance package layout is planned", "The ssot-conformance package layout is tracked as a T2 conformance implementation target.", ["feat:conformance.package-layout"], ["tst:pytest.conformance.package-layout"], ["evd:t2.conformance.package-layout.pytest"]),
|
|
79
|
+
],
|
|
80
|
+
"plugin": [
|
|
81
|
+
("clm:conformance.pytest-plugin.t2", "Conformance pytest plugin is planned", "The pytest plugin integration is tracked as a T2 conformance implementation target.", ["feat:conformance.pytest-plugin"], ["tst:pytest.conformance.pytest-plugin"], ["evd:t2.conformance.pytest-plugin.pytest"]),
|
|
82
|
+
("clm:conformance.case-discovery.t2", "Conformance case discovery is planned", "The conformance profile and case discovery behavior is tracked as a T2 target.", ["feat:conformance.case-discovery"], ["tst:pytest.conformance.pytest-plugin"], ["evd:t2.conformance.pytest-plugin.pytest"]),
|
|
83
|
+
],
|
|
84
|
+
"registry": [
|
|
85
|
+
("clm:conformance.registry-contract.t2", "Registry conformance contract is planned", "The registry conformance test family is tracked as a T2 target.", ["feat:conformance.registry-contract"], ["tst:pytest.conformance.registry-contract"], ["evd:t2.conformance.registry-contract.pytest"]),
|
|
86
|
+
],
|
|
87
|
+
"document": [
|
|
88
|
+
("clm:conformance.document-contract.t2", "Document conformance contract is planned", "The ADR and SPEC document conformance test family is tracked as a T2 target.", ["feat:conformance.document-contract"], ["tst:pytest.conformance.document-contract"], ["evd:t2.conformance.document-contract.pytest"]),
|
|
89
|
+
],
|
|
90
|
+
"id": [
|
|
91
|
+
("clm:conformance.id-contract.t2", "Normalized ID conformance contract is planned", "The normalized ID conformance test family is tracked as a T2 target.", ["feat:conformance.id-contract"], ["tst:pytest.conformance.id-contract"], ["evd:t2.conformance.id-contract.pytest"]),
|
|
92
|
+
],
|
|
93
|
+
"spec-adr": [
|
|
94
|
+
("clm:conformance.spec-adr-contract.t2", "SPEC-to-ADR conformance contract is planned", "The SPEC-to-ADR conformance test family is tracked as a T2 target.", ["feat:conformance.spec-adr-contract"], ["tst:pytest.conformance.spec-adr-contract"], ["evd:t2.conformance.spec-adr-contract.pytest"]),
|
|
95
|
+
],
|
|
96
|
+
"feature-spec": [
|
|
97
|
+
("clm:conformance.feature-spec-contract.t2", "Feature-to-SPEC conformance contract is planned", "The feature-to-SPEC conformance test family is tracked as a T2 target.", ["feat:conformance.feature-spec-contract"], ["tst:pytest.conformance.feature-spec-contract"], ["evd:t2.conformance.feature-spec-contract.pytest"]),
|
|
98
|
+
],
|
|
99
|
+
"proof-chain": [
|
|
100
|
+
("clm:conformance.proof-chain-contract.t2", "Proof-chain conformance contract is planned", "The feature claim test evidence proof-chain conformance family is tracked as a T2 target.", ["feat:conformance.proof-chain-contract"], ["tst:pytest.conformance.proof-chain-contract"], ["evd:t2.conformance.proof-chain-contract.pytest"]),
|
|
101
|
+
],
|
|
102
|
+
"boundary-release": [
|
|
103
|
+
("clm:conformance.boundary-release-contract.t2", "Boundary and release conformance contract is planned", "The boundary and release conformance test family is tracked as a T2 target.", ["feat:conformance.boundary-release-contract"], ["tst:pytest.conformance.boundary-release-contract"], ["evd:t2.conformance.boundary-release-contract.pytest"]),
|
|
104
|
+
],
|
|
105
|
+
"scaffold": [
|
|
106
|
+
("clm:conformance.scaffold-dry-run.t2", "Conformance scaffold dry-run is planned", "The scaffold dry-run planner is tracked as a T2 target.", ["feat:conformance.scaffold-dry-run"], ["tst:pytest.conformance.scaffold"], ["evd:t2.conformance.scaffold.pytest"]),
|
|
107
|
+
("clm:conformance.scaffold-apply.t2", "Conformance scaffold apply is planned", "The scaffold apply mutation path is tracked as a T2 target.", ["feat:conformance.scaffold-apply"], ["tst:pytest.conformance.scaffold"], ["evd:t2.conformance.scaffold.pytest"]),
|
|
108
|
+
("clm:conformance.scaffold-idempotency.t2", "Conformance scaffold idempotency is planned", "The scaffold idempotency behavior is tracked as a T2 target.", ["feat:conformance.scaffold-idempotency"], ["tst:pytest.conformance.scaffold"], ["evd:t2.conformance.scaffold.pytest"]),
|
|
109
|
+
],
|
|
110
|
+
"evidence-output": [
|
|
111
|
+
("clm:conformance.evidence-output.t2", "Conformance evidence output is planned", "The conformance evidence output behavior is tracked as a T2 target.", ["feat:conformance.evidence-output"], ["tst:pytest.conformance.evidence-output"], ["evd:t2.conformance.evidence-output.pytest"]),
|
|
112
|
+
],
|
|
113
|
+
"cli": [
|
|
114
|
+
("clm:cli.conformance-surface.t2", "CLI conformance surface is planned", "The ssot conformance CLI surface is tracked as a T2 target.", ["feat:cli.conformance-surface"], ["tst:pytest.cli.conformance-surface"], ["evd:t2.cli.conformance-surface.pytest"]),
|
|
115
|
+
],
|
|
116
|
+
"command-runner": [
|
|
117
|
+
("clm:conformance.command-runner.t2", "Registry-driven conformance command runner is planned", "The suite-agnostic command execution lane is tracked as a T2 conformance target.", ["feat:conformance.command-runner"], ["tst:command.conformance.command-runner"], ["evd:t2.conformance.command-runner.command"]),
|
|
118
|
+
],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_TESTS = {
|
|
122
|
+
"package-layout": [
|
|
123
|
+
("tst:pytest.conformance.package-layout", "Conformance package layout tests", "tests/unit/test_conformance_package_layout.py", ["feat:conformance.package-layout"], ["clm:conformance.package-layout.t2"], ["evd:t2.conformance.package-layout.pytest"]),
|
|
124
|
+
],
|
|
125
|
+
"plugin": [
|
|
126
|
+
("tst:pytest.conformance.pytest-plugin", "Conformance pytest plugin integration tests", "tests/integration/test_conformance_pytest_plugin.py", ["feat:conformance.pytest-plugin", "feat:conformance.case-discovery"], ["clm:conformance.pytest-plugin.t2", "clm:conformance.case-discovery.t2"], ["evd:t2.conformance.pytest-plugin.pytest"]),
|
|
127
|
+
],
|
|
128
|
+
"registry": [
|
|
129
|
+
("tst:pytest.conformance.registry-contract", "Registry conformance contract tests", "pkgs/ssot-conformance/src/ssot_conformance/cases/test_registry_contract.py", ["feat:conformance.registry-contract"], ["clm:conformance.registry-contract.t2"], ["evd:t2.conformance.registry-contract.pytest"]),
|
|
130
|
+
],
|
|
131
|
+
"document": [
|
|
132
|
+
("tst:pytest.conformance.document-contract", "Document conformance contract tests", "pkgs/ssot-conformance/src/ssot_conformance/cases/test_document_contract.py", ["feat:conformance.document-contract"], ["clm:conformance.document-contract.t2"], ["evd:t2.conformance.document-contract.pytest"]),
|
|
133
|
+
],
|
|
134
|
+
"id": [
|
|
135
|
+
("tst:pytest.conformance.id-contract", "Normalized ID conformance contract tests", "pkgs/ssot-conformance/src/ssot_conformance/cases/test_id_contract.py", ["feat:conformance.id-contract"], ["clm:conformance.id-contract.t2"], ["evd:t2.conformance.id-contract.pytest"]),
|
|
136
|
+
],
|
|
137
|
+
"spec-adr": [
|
|
138
|
+
("tst:pytest.conformance.spec-adr-contract", "SPEC-to-ADR conformance contract tests", "pkgs/ssot-conformance/src/ssot_conformance/cases/test_spec_adr_contract.py", ["feat:conformance.spec-adr-contract"], ["clm:conformance.spec-adr-contract.t2"], ["evd:t2.conformance.spec-adr-contract.pytest"]),
|
|
139
|
+
],
|
|
140
|
+
"feature-spec": [
|
|
141
|
+
("tst:pytest.conformance.feature-spec-contract", "Feature-to-SPEC conformance contract tests", "pkgs/ssot-conformance/src/ssot_conformance/cases/test_feature_spec_contract.py", ["feat:conformance.feature-spec-contract"], ["clm:conformance.feature-spec-contract.t2"], ["evd:t2.conformance.feature-spec-contract.pytest"]),
|
|
142
|
+
],
|
|
143
|
+
"proof-chain": [
|
|
144
|
+
("tst:pytest.conformance.proof-chain-contract", "Proof-chain conformance contract tests", "pkgs/ssot-conformance/src/ssot_conformance/cases/test_proof_chain_contract.py", ["feat:conformance.proof-chain-contract"], ["clm:conformance.proof-chain-contract.t2"], ["evd:t2.conformance.proof-chain-contract.pytest"]),
|
|
145
|
+
],
|
|
146
|
+
"boundary-release": [
|
|
147
|
+
("tst:pytest.conformance.boundary-release-contract", "Boundary and release conformance contract tests", "pkgs/ssot-conformance/src/ssot_conformance/cases/test_boundary_release_contract.py", ["feat:conformance.boundary-release-contract"], ["clm:conformance.boundary-release-contract.t2"], ["evd:t2.conformance.boundary-release-contract.pytest"]),
|
|
148
|
+
],
|
|
149
|
+
"scaffold": [
|
|
150
|
+
("tst:pytest.conformance.scaffold", "Conformance scaffold tests", "tests/unit/test_conformance_scaffold.py", ["feat:conformance.scaffold-dry-run", "feat:conformance.scaffold-apply", "feat:conformance.scaffold-idempotency"], ["clm:conformance.scaffold-dry-run.t2", "clm:conformance.scaffold-apply.t2", "clm:conformance.scaffold-idempotency.t2"], ["evd:t2.conformance.scaffold.pytest"]),
|
|
151
|
+
],
|
|
152
|
+
"evidence-output": [
|
|
153
|
+
("tst:pytest.conformance.evidence-output", "Conformance evidence output tests", "tests/unit/test_conformance_evidence_output.py", ["feat:conformance.evidence-output"], ["clm:conformance.evidence-output.t2"], ["evd:t2.conformance.evidence-output.pytest"]),
|
|
154
|
+
],
|
|
155
|
+
"cli": [
|
|
156
|
+
("tst:pytest.cli.conformance-surface", "CLI conformance surface tests", "tests/integration/test_cli_conformance.py", ["feat:cli.conformance-surface"], ["clm:cli.conformance-surface.t2"], ["evd:t2.cli.conformance-surface.pytest"]),
|
|
157
|
+
],
|
|
158
|
+
"command-runner": [
|
|
159
|
+
("tst:command.conformance.command-runner", "Registry-driven conformance command runner tests", "tests/integration/test_cli_registry_execution.py", ["feat:conformance.command-runner"], ["clm:conformance.command-runner.t2"], ["evd:t2.conformance.command-runner.command"]),
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_EVIDENCE = {
|
|
164
|
+
"package-layout": [
|
|
165
|
+
("evd:t2.conformance.package-layout.pytest", "Package layout pytest evidence", ".ssot/evidence/conformance/conformance-package-layout-pytest.json", ["clm:conformance.package-layout.t2"], ["tst:pytest.conformance.package-layout"]),
|
|
166
|
+
],
|
|
167
|
+
"plugin": [
|
|
168
|
+
("evd:t2.conformance.pytest-plugin.pytest", "Pytest plugin integration evidence", ".ssot/evidence/conformance/conformance-pytest-plugin-pytest.json", ["clm:conformance.pytest-plugin.t2", "clm:conformance.case-discovery.t2"], ["tst:pytest.conformance.pytest-plugin"]),
|
|
169
|
+
],
|
|
170
|
+
"registry": [
|
|
171
|
+
("evd:t2.conformance.registry-contract.pytest", "Registry contract pytest evidence", ".ssot/evidence/conformance/conformance-registry-contract-pytest.json", ["clm:conformance.registry-contract.t2"], ["tst:pytest.conformance.registry-contract"]),
|
|
172
|
+
],
|
|
173
|
+
"document": [
|
|
174
|
+
("evd:t2.conformance.document-contract.pytest", "Document contract pytest evidence", ".ssot/evidence/conformance/conformance-document-contract-pytest.json", ["clm:conformance.document-contract.t2"], ["tst:pytest.conformance.document-contract"]),
|
|
175
|
+
],
|
|
176
|
+
"id": [
|
|
177
|
+
("evd:t2.conformance.id-contract.pytest", "ID contract pytest evidence", ".ssot/evidence/conformance/conformance-id-contract-pytest.json", ["clm:conformance.id-contract.t2"], ["tst:pytest.conformance.id-contract"]),
|
|
178
|
+
],
|
|
179
|
+
"spec-adr": [
|
|
180
|
+
("evd:t2.conformance.spec-adr-contract.pytest", "SPEC-to-ADR contract pytest evidence", ".ssot/evidence/conformance/conformance-spec-adr-contract-pytest.json", ["clm:conformance.spec-adr-contract.t2"], ["tst:pytest.conformance.spec-adr-contract"]),
|
|
181
|
+
],
|
|
182
|
+
"feature-spec": [
|
|
183
|
+
("evd:t2.conformance.feature-spec-contract.pytest", "Feature-to-SPEC contract pytest evidence", ".ssot/evidence/conformance/conformance-feature-spec-contract-pytest.json", ["clm:conformance.feature-spec-contract.t2"], ["tst:pytest.conformance.feature-spec-contract"]),
|
|
184
|
+
],
|
|
185
|
+
"proof-chain": [
|
|
186
|
+
("evd:t2.conformance.proof-chain-contract.pytest", "Proof-chain contract pytest evidence", ".ssot/evidence/conformance/conformance-proof-chain-contract-pytest.json", ["clm:conformance.proof-chain-contract.t2"], ["tst:pytest.conformance.proof-chain-contract"]),
|
|
187
|
+
],
|
|
188
|
+
"boundary-release": [
|
|
189
|
+
("evd:t2.conformance.boundary-release-contract.pytest", "Boundary and release contract pytest evidence", ".ssot/evidence/conformance/conformance-boundary-release-contract-pytest.json", ["clm:conformance.boundary-release-contract.t2"], ["tst:pytest.conformance.boundary-release-contract"]),
|
|
190
|
+
],
|
|
191
|
+
"scaffold": [
|
|
192
|
+
("evd:t2.conformance.scaffold.pytest", "Scaffold pytest evidence", ".ssot/evidence/conformance/conformance-scaffold-pytest.json", ["clm:conformance.scaffold-dry-run.t2", "clm:conformance.scaffold-apply.t2", "clm:conformance.scaffold-idempotency.t2"], ["tst:pytest.conformance.scaffold"]),
|
|
193
|
+
],
|
|
194
|
+
"evidence-output": [
|
|
195
|
+
("evd:t2.conformance.evidence-output.pytest", "Evidence output pytest evidence", ".ssot/evidence/conformance/conformance-evidence-output-pytest.json", ["clm:conformance.evidence-output.t2"], ["tst:pytest.conformance.evidence-output"]),
|
|
196
|
+
],
|
|
197
|
+
"cli": [
|
|
198
|
+
("evd:t2.cli.conformance-surface.pytest", "CLI conformance surface pytest evidence", ".ssot/evidence/conformance/cli-conformance-surface-pytest.json", ["clm:cli.conformance-surface.t2"], ["tst:pytest.cli.conformance-surface"]),
|
|
199
|
+
],
|
|
200
|
+
"command-runner": [
|
|
201
|
+
("evd:t2.conformance.command-runner.command", "Registry-driven conformance command evidence", ".ssot/evidence/conformance/conformance-command-runner-command.json", ["clm:conformance.command-runner.t2"], ["tst:command.conformance.command-runner"]),
|
|
202
|
+
],
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_TEST_KIND_OVERRIDES = {
|
|
206
|
+
"tst:command.conformance.command-runner": "command",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_EVIDENCE_KIND_OVERRIDES = {
|
|
210
|
+
"evd:t2.conformance.command-runner.command": "command",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _build_execution_row(test_id: str, path: str, kind: str) -> dict[str, object]:
|
|
215
|
+
packaged_case_root = "pkgs/ssot-conformance/src/ssot_conformance/cases/"
|
|
216
|
+
env: dict[str, str] = {}
|
|
217
|
+
if path.startswith(packaged_case_root) and path.endswith(".py"):
|
|
218
|
+
module_name = f"ssot_conformance.cases.{Path(path).stem}"
|
|
219
|
+
argv = [
|
|
220
|
+
"python",
|
|
221
|
+
"-m",
|
|
222
|
+
"pytest",
|
|
223
|
+
"--pyargs",
|
|
224
|
+
module_name,
|
|
225
|
+
"-q",
|
|
226
|
+
"-p",
|
|
227
|
+
"ssot_conformance.plugin",
|
|
228
|
+
"--ssot-repo-root=.",
|
|
229
|
+
]
|
|
230
|
+
env["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = "1"
|
|
231
|
+
else:
|
|
232
|
+
argv = ["python", "-m", "pytest", path, "-q"]
|
|
233
|
+
return {
|
|
234
|
+
"mode": "command",
|
|
235
|
+
"argv": argv,
|
|
236
|
+
"cwd": ".",
|
|
237
|
+
"env": env,
|
|
238
|
+
"timeout_seconds": 600,
|
|
239
|
+
"success": {"type": "exit_code", "expected": 0},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def list_profiles() -> list[dict[str, object]]:
|
|
244
|
+
profiles = []
|
|
245
|
+
for profile_name, families in PROFILE_DEFINITIONS.items():
|
|
246
|
+
profiles.append(
|
|
247
|
+
{
|
|
248
|
+
"profile": profile_name,
|
|
249
|
+
"families": list(families),
|
|
250
|
+
"case_count": sum(len(_TESTS[family]) for family in families),
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
return profiles
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def resolve_selected_families(profiles: list[str] | None = None) -> list[str]:
|
|
257
|
+
if not profiles:
|
|
258
|
+
return list(PROFILE_DEFINITIONS["all"])
|
|
259
|
+
selected: list[str] = []
|
|
260
|
+
for profile in profiles:
|
|
261
|
+
if profile not in PROFILE_DEFINITIONS:
|
|
262
|
+
raise ValueError(f"Unknown conformance profile: {profile}")
|
|
263
|
+
for family in PROFILE_DEFINITIONS[profile]:
|
|
264
|
+
if family not in selected:
|
|
265
|
+
selected.append(family)
|
|
266
|
+
return selected
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _feature_row(values: tuple[str, str, str, list[str], list[str], list[str]]) -> dict[str, object]:
|
|
270
|
+
feature_id, title, description, spec_ids, claim_ids, test_ids = values
|
|
271
|
+
return {
|
|
272
|
+
"id": feature_id,
|
|
273
|
+
"title": title,
|
|
274
|
+
"description": description,
|
|
275
|
+
"implementation_status": "absent",
|
|
276
|
+
"lifecycle": {"stage": "active", "replacement_feature_ids": [], "note": None},
|
|
277
|
+
"plan": {"horizon": "backlog", "slot": None, "target_claim_tier": "T2", "target_lifecycle_stage": "active"},
|
|
278
|
+
"spec_ids": list(spec_ids),
|
|
279
|
+
"claim_ids": list(claim_ids),
|
|
280
|
+
"test_ids": list(test_ids),
|
|
281
|
+
"requires": [],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _claim_row(values: tuple[str, str, str, list[str], list[str], list[str]]) -> dict[str, object]:
|
|
286
|
+
claim_id, title, description, feature_ids, test_ids, evidence_ids = values
|
|
287
|
+
return {
|
|
288
|
+
"id": claim_id,
|
|
289
|
+
"title": title,
|
|
290
|
+
"status": "proposed",
|
|
291
|
+
"tier": "T2",
|
|
292
|
+
"kind": "conformance",
|
|
293
|
+
"description": description,
|
|
294
|
+
"feature_ids": list(feature_ids),
|
|
295
|
+
"test_ids": list(test_ids),
|
|
296
|
+
"evidence_ids": list(evidence_ids),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _test_row(values: tuple[str, str, str, list[str], list[str], list[str]]) -> dict[str, object]:
|
|
301
|
+
test_id, title, path, feature_ids, claim_ids, evidence_ids = values
|
|
302
|
+
kind = _TEST_KIND_OVERRIDES.get(test_id, "pytest")
|
|
303
|
+
return {
|
|
304
|
+
"id": test_id,
|
|
305
|
+
"title": title,
|
|
306
|
+
"status": "planned",
|
|
307
|
+
"kind": kind,
|
|
308
|
+
"path": path,
|
|
309
|
+
"feature_ids": list(feature_ids),
|
|
310
|
+
"claim_ids": list(claim_ids),
|
|
311
|
+
"evidence_ids": list(evidence_ids),
|
|
312
|
+
"execution": _build_execution_row(test_id, path, kind),
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _evidence_row(values: tuple[str, str, str, list[str], list[str]]) -> dict[str, object]:
|
|
317
|
+
evidence_id, title, path, claim_ids, test_ids = values
|
|
318
|
+
return {
|
|
319
|
+
"id": evidence_id,
|
|
320
|
+
"title": title,
|
|
321
|
+
"status": "planned",
|
|
322
|
+
"kind": _EVIDENCE_KIND_OVERRIDES.get(evidence_id, "pytest"),
|
|
323
|
+
"tier": "T2",
|
|
324
|
+
"path": path,
|
|
325
|
+
"claim_ids": list(claim_ids),
|
|
326
|
+
"test_ids": list(test_ids),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def build_catalog_slice(profiles: list[str] | None = None) -> dict[str, object]:
|
|
331
|
+
families = resolve_selected_families(profiles)
|
|
332
|
+
payload = {
|
|
333
|
+
"catalog_version": CATALOG_VERSION,
|
|
334
|
+
"profiles": profiles or ["all"],
|
|
335
|
+
"families": families,
|
|
336
|
+
"features": [],
|
|
337
|
+
"claims": [],
|
|
338
|
+
"tests": [],
|
|
339
|
+
"evidence": [],
|
|
340
|
+
}
|
|
341
|
+
for family in families:
|
|
342
|
+
payload["features"].extend(_feature_row(values) for values in _FEATURES[family])
|
|
343
|
+
payload["claims"].extend(_claim_row(values) for values in _CLAIMS[family])
|
|
344
|
+
payload["tests"].extend(_test_row(values) for values in _TESTS[family])
|
|
345
|
+
payload["evidence"].extend(_evidence_row(values) for values in _EVIDENCE[family])
|
|
346
|
+
return deepcopy(payload)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .catalog import build_catalog_slice
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def discover_cases(profiles: list[str] | None = None) -> dict[str, object]:
|
|
7
|
+
catalog = build_catalog_slice(profiles)
|
|
8
|
+
return {
|
|
9
|
+
"passed": True,
|
|
10
|
+
"catalog_version": catalog["catalog_version"],
|
|
11
|
+
"profiles": catalog["profiles"],
|
|
12
|
+
"families": catalog["families"],
|
|
13
|
+
"cases": [
|
|
14
|
+
{
|
|
15
|
+
"id": row["id"],
|
|
16
|
+
"title": row["title"],
|
|
17
|
+
"kind": row["kind"],
|
|
18
|
+
"path": row["path"],
|
|
19
|
+
"feature_ids": row["feature_ids"],
|
|
20
|
+
"claim_ids": row["claim_ids"],
|
|
21
|
+
"evidence_ids": row["evidence_ids"],
|
|
22
|
+
}
|
|
23
|
+
for row in catalog["tests"]
|
|
24
|
+
],
|
|
25
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ssot_registry.util.jcs import dump_jcs_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_evidence_output(
|
|
9
|
+
*,
|
|
10
|
+
repo_root: str,
|
|
11
|
+
selected_profiles: list[str],
|
|
12
|
+
selected_families: list[str],
|
|
13
|
+
cases: list[dict[str, object]],
|
|
14
|
+
) -> dict[str, object]:
|
|
15
|
+
summary = {"passed": 0, "failed": 0, "skipped": 0, "total": len(cases)}
|
|
16
|
+
for row in cases:
|
|
17
|
+
outcome = str(row.get("outcome", "unknown"))
|
|
18
|
+
if outcome in summary:
|
|
19
|
+
summary[outcome] += 1
|
|
20
|
+
return {
|
|
21
|
+
"kind": "ssot-conformance-evidence",
|
|
22
|
+
"repo_root": repo_root,
|
|
23
|
+
"profiles": selected_profiles,
|
|
24
|
+
"families": selected_families,
|
|
25
|
+
"summary": summary,
|
|
26
|
+
"cases": cases,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_evidence_output(path: str | Path, payload: dict[str, object]) -> str:
|
|
31
|
+
target = Path(path)
|
|
32
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
target.write_text(dump_jcs_json(payload), encoding="utf-8")
|
|
34
|
+
return target.as_posix()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
try: # pragma: no cover - import depends on environment
|
|
7
|
+
import pytest
|
|
8
|
+
except ModuleNotFoundError: # pragma: no cover - import depends on environment
|
|
9
|
+
pytest = None
|
|
10
|
+
|
|
11
|
+
from .catalog import resolve_selected_families
|
|
12
|
+
from .evidence import build_evidence_output, write_evidence_output
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def pytest_addoption(parser: Any) -> None:
|
|
16
|
+
group = parser.getgroup("ssot-conformance")
|
|
17
|
+
group.addoption("--ssot-repo-root", action="store", default=".", help="Target repository root for SSOT conformance evaluation.")
|
|
18
|
+
group.addoption(
|
|
19
|
+
"--ssot-conformance-profile",
|
|
20
|
+
action="append",
|
|
21
|
+
default=[],
|
|
22
|
+
help="Conformance profile or family to execute. May be repeated.",
|
|
23
|
+
)
|
|
24
|
+
group.addoption(
|
|
25
|
+
"--ssot-conformance-evidence-output",
|
|
26
|
+
action="store",
|
|
27
|
+
default=None,
|
|
28
|
+
help="Write machine-readable conformance evidence JSON to this path.",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def pytest_configure(config: Any) -> None:
|
|
33
|
+
config._ssot_conformance_reports = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def pytest_collection_modifyitems(config: Any, items: list[Any]) -> None:
|
|
37
|
+
selected_families = set(resolve_selected_families(config.getoption("ssot_conformance_profile")))
|
|
38
|
+
kept: list[Any] = []
|
|
39
|
+
deselected: list[Any] = []
|
|
40
|
+
for item in items:
|
|
41
|
+
family = getattr(item.module, "CONFORMANCE_FAMILY", None)
|
|
42
|
+
if family is None or family in selected_families:
|
|
43
|
+
kept.append(item)
|
|
44
|
+
else:
|
|
45
|
+
deselected.append(item)
|
|
46
|
+
if deselected:
|
|
47
|
+
config.hook.pytest_deselected(items=deselected)
|
|
48
|
+
items[:] = kept
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if pytest is not None:
|
|
52
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
53
|
+
def pytest_runtest_makereport(item: Any, call: Any) -> Any:
|
|
54
|
+
outcome = yield
|
|
55
|
+
report = outcome.get_result()
|
|
56
|
+
if report.when != "call":
|
|
57
|
+
return
|
|
58
|
+
item.config._ssot_conformance_reports.append(
|
|
59
|
+
{
|
|
60
|
+
"nodeid": report.nodeid,
|
|
61
|
+
"family": getattr(item.module, "CONFORMANCE_FAMILY", None),
|
|
62
|
+
"outcome": report.outcome,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def pytest_sessionfinish(session: Any, exitstatus: int) -> None:
|
|
68
|
+
output = session.config.getoption("ssot_conformance_evidence_output")
|
|
69
|
+
if output is None:
|
|
70
|
+
return
|
|
71
|
+
selected_profiles = session.config.getoption("ssot_conformance_profile") or ["all"]
|
|
72
|
+
selected_families = resolve_selected_families(selected_profiles)
|
|
73
|
+
payload = build_evidence_output(
|
|
74
|
+
repo_root=str(Path(session.config.getoption("ssot_repo_root")).resolve()),
|
|
75
|
+
selected_profiles=list(selected_profiles),
|
|
76
|
+
selected_families=selected_families,
|
|
77
|
+
cases=list(session.config._ssot_conformance_reports),
|
|
78
|
+
)
|
|
79
|
+
write_evidence_output(output, payload)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if pytest is not None:
|
|
83
|
+
@pytest.fixture(scope="session")
|
|
84
|
+
def ssot_repo_root(pytestconfig: Any) -> Path:
|
|
85
|
+
return Path(pytestconfig.getoption("ssot_repo_root")).resolve()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.fixture(scope="session")
|
|
89
|
+
def ssot_registry(ssot_repo_root: Path) -> dict[str, object]:
|
|
90
|
+
import json
|
|
91
|
+
|
|
92
|
+
return json.loads((ssot_repo_root / ".ssot" / "registry.json").read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .catalog import resolve_selected_families
|
|
10
|
+
from .evidence import build_evidence_output, write_evidence_output
|
|
11
|
+
|
|
12
|
+
_PLUGIN_MODULE_NAME = "ssot_conformance.plugin"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_pytest_cases(
|
|
16
|
+
*,
|
|
17
|
+
repo_root: str | Path,
|
|
18
|
+
profiles: list[str] | None = None,
|
|
19
|
+
evidence_output: str | Path | None = None,
|
|
20
|
+
pytest_args: list[str] | None = None,
|
|
21
|
+
) -> dict[str, object]:
|
|
22
|
+
try:
|
|
23
|
+
import pytest
|
|
24
|
+
except ModuleNotFoundError as exc: # pragma: no cover - depends on test environment
|
|
25
|
+
raise ValueError("pytest is required for `ssot conformance run`; install dev dependencies or use `uv run pytest`") from exc
|
|
26
|
+
|
|
27
|
+
import importlib.resources as resources
|
|
28
|
+
|
|
29
|
+
resolved_repo_root = Path(repo_root).resolve()
|
|
30
|
+
selected_profiles = profiles or ["all"]
|
|
31
|
+
selected_families = resolve_selected_families(profiles)
|
|
32
|
+
case_root = resources.files("ssot_conformance.cases")
|
|
33
|
+
args = [
|
|
34
|
+
str(case_root),
|
|
35
|
+
"-p",
|
|
36
|
+
_PLUGIN_MODULE_NAME,
|
|
37
|
+
f"--ssot-repo-root={resolved_repo_root}",
|
|
38
|
+
]
|
|
39
|
+
for profile in selected_profiles:
|
|
40
|
+
args.append(f"--ssot-conformance-profile={profile}")
|
|
41
|
+
if evidence_output is not None:
|
|
42
|
+
args.append(f"--ssot-conformance-evidence-output={evidence_output}")
|
|
43
|
+
args.extend(pytest_args or [])
|
|
44
|
+
|
|
45
|
+
stdout_buffer = io.StringIO()
|
|
46
|
+
stderr_buffer = io.StringIO()
|
|
47
|
+
previous_plugin_autoload = os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
|
|
48
|
+
os.environ["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = "1"
|
|
49
|
+
with contextlib.redirect_stdout(stdout_buffer), contextlib.redirect_stderr(stderr_buffer):
|
|
50
|
+
try:
|
|
51
|
+
exit_code = pytest.main(args)
|
|
52
|
+
finally:
|
|
53
|
+
if previous_plugin_autoload is None:
|
|
54
|
+
os.environ.pop("PYTEST_DISABLE_PLUGIN_AUTOLOAD", None)
|
|
55
|
+
else:
|
|
56
|
+
os.environ["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = previous_plugin_autoload
|
|
57
|
+
payload: dict[str, object] = {
|
|
58
|
+
"runner": "pytest",
|
|
59
|
+
"passed": exit_code == 0,
|
|
60
|
+
"pytest_exit_code": exit_code,
|
|
61
|
+
"profiles": selected_profiles,
|
|
62
|
+
"families": selected_families,
|
|
63
|
+
"case_root": str(case_root),
|
|
64
|
+
}
|
|
65
|
+
if stdout_buffer.getvalue():
|
|
66
|
+
payload["pytest_stdout"] = stdout_buffer.getvalue()
|
|
67
|
+
if stderr_buffer.getvalue():
|
|
68
|
+
payload["pytest_stderr"] = stderr_buffer.getvalue()
|
|
69
|
+
if evidence_output is not None:
|
|
70
|
+
evidence_path = Path(evidence_output)
|
|
71
|
+
if evidence_path.exists():
|
|
72
|
+
payload["evidence_output"] = evidence_path.as_posix()
|
|
73
|
+
else:
|
|
74
|
+
payload["evidence_output"] = write_evidence_output(
|
|
75
|
+
evidence_path,
|
|
76
|
+
build_evidence_output(
|
|
77
|
+
repo_root=str(resolved_repo_root),
|
|
78
|
+
selected_profiles=selected_profiles,
|
|
79
|
+
selected_families=selected_families,
|
|
80
|
+
cases=[],
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
return payload
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_command_suite(
|
|
87
|
+
*,
|
|
88
|
+
repo_root: str | Path,
|
|
89
|
+
profiles: list[str] | None = None,
|
|
90
|
+
evidence_output: str | Path | None = None,
|
|
91
|
+
command: list[str],
|
|
92
|
+
) -> dict[str, object]:
|
|
93
|
+
if not command:
|
|
94
|
+
raise ValueError("command runner requires a command after `--command`")
|
|
95
|
+
|
|
96
|
+
resolved_repo_root = Path(repo_root).resolve()
|
|
97
|
+
selected_profiles = profiles or ["all"]
|
|
98
|
+
selected_families = resolve_selected_families(profiles)
|
|
99
|
+
result = subprocess.run(
|
|
100
|
+
command,
|
|
101
|
+
cwd=str(resolved_repo_root),
|
|
102
|
+
text=True,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
check=False,
|
|
105
|
+
)
|
|
106
|
+
outcome = "passed" if result.returncode == 0 else "failed"
|
|
107
|
+
case = {
|
|
108
|
+
"nodeid": "command-suite",
|
|
109
|
+
"runner": "command",
|
|
110
|
+
"command": list(command),
|
|
111
|
+
"outcome": outcome,
|
|
112
|
+
"returncode": result.returncode,
|
|
113
|
+
}
|
|
114
|
+
if result.stdout:
|
|
115
|
+
case["stdout"] = result.stdout
|
|
116
|
+
if result.stderr:
|
|
117
|
+
case["stderr"] = result.stderr
|
|
118
|
+
payload: dict[str, object] = {
|
|
119
|
+
"runner": "command",
|
|
120
|
+
"passed": result.returncode == 0,
|
|
121
|
+
"command_exit_code": result.returncode,
|
|
122
|
+
"command": list(command),
|
|
123
|
+
"profiles": selected_profiles,
|
|
124
|
+
"families": selected_families,
|
|
125
|
+
}
|
|
126
|
+
if evidence_output is not None:
|
|
127
|
+
payload["evidence_output"] = write_evidence_output(
|
|
128
|
+
evidence_output,
|
|
129
|
+
build_evidence_output(
|
|
130
|
+
repo_root=str(resolved_repo_root),
|
|
131
|
+
selected_profiles=selected_profiles,
|
|
132
|
+
selected_families=selected_families,
|
|
133
|
+
cases=[case],
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
payload["cases"] = [case]
|
|
138
|
+
return payload
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ssot_registry.api import create_entity, validate_registry
|
|
7
|
+
|
|
8
|
+
from .catalog import build_catalog_slice, resolve_selected_families
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _ids(rows: list[dict[str, object]]) -> set[str]:
|
|
12
|
+
return {str(row["id"]) for row in rows}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plan_scaffold(
|
|
16
|
+
path: str | Path,
|
|
17
|
+
*,
|
|
18
|
+
profiles: list[str] | None = None,
|
|
19
|
+
include_claims: bool = False,
|
|
20
|
+
include_evidence: bool = False,
|
|
21
|
+
) -> dict[str, object]:
|
|
22
|
+
registry_report = validate_registry(path)
|
|
23
|
+
if not registry_report["passed"]:
|
|
24
|
+
raise ValueError("Target registry must validate before conformance scaffold planning")
|
|
25
|
+
|
|
26
|
+
catalog = build_catalog_slice(profiles)
|
|
27
|
+
registry_path = Path(path) / ".ssot" / "registry.json" if Path(path).is_dir() else Path(path)
|
|
28
|
+
repo_root = registry_path.parent.parent if registry_path.name == "registry.json" else Path(path)
|
|
29
|
+
import json
|
|
30
|
+
|
|
31
|
+
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
|
32
|
+
existing = {section: _ids(registry.get(section, [])) for section in ("features", "claims", "tests", "evidence")}
|
|
33
|
+
|
|
34
|
+
missing = {
|
|
35
|
+
"features": [row["id"] for row in catalog["features"] if row["id"] not in existing["features"]],
|
|
36
|
+
"claims": [row["id"] for row in catalog["claims"] if row["id"] not in existing["claims"]],
|
|
37
|
+
"tests": [row["id"] for row in catalog["tests"] if row["id"] not in existing["tests"]],
|
|
38
|
+
"evidence": [row["id"] for row in catalog["evidence"] if row["id"] not in existing["evidence"]],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
planned_feature_ids = set(existing["features"]) | set(missing["features"])
|
|
42
|
+
planned_claim_ids = set(existing["claims"]) | (set(missing["claims"]) if include_claims else set())
|
|
43
|
+
planned_evidence_ids = set(existing["evidence"]) | (set(missing["evidence"]) if include_evidence else set())
|
|
44
|
+
|
|
45
|
+
blocked_tests: list[dict[str, object]] = []
|
|
46
|
+
creatable_tests: list[str] = []
|
|
47
|
+
for row in catalog["tests"]:
|
|
48
|
+
test_id = str(row["id"])
|
|
49
|
+
if test_id in existing["tests"]:
|
|
50
|
+
continue
|
|
51
|
+
reasons: list[str] = []
|
|
52
|
+
for feature_id in row["feature_ids"]:
|
|
53
|
+
if feature_id not in planned_feature_ids:
|
|
54
|
+
reasons.append(f"missing feature {feature_id}")
|
|
55
|
+
for claim_id in row["claim_ids"]:
|
|
56
|
+
if claim_id not in planned_claim_ids:
|
|
57
|
+
reasons.append(f"missing claim {claim_id}")
|
|
58
|
+
for evidence_id in row["evidence_ids"]:
|
|
59
|
+
if evidence_id not in planned_evidence_ids:
|
|
60
|
+
reasons.append(f"missing evidence {evidence_id}")
|
|
61
|
+
if reasons:
|
|
62
|
+
blocked_tests.append({"id": test_id, "reasons": reasons})
|
|
63
|
+
else:
|
|
64
|
+
creatable_tests.append(test_id)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"passed": True,
|
|
68
|
+
"repo_root": repo_root.as_posix(),
|
|
69
|
+
"profiles": catalog["profiles"],
|
|
70
|
+
"families": resolve_selected_families(profiles),
|
|
71
|
+
"missing": missing,
|
|
72
|
+
"creatable_test_ids": creatable_tests,
|
|
73
|
+
"blocked_tests": blocked_tests,
|
|
74
|
+
"include_claims": include_claims,
|
|
75
|
+
"include_evidence": include_evidence,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def apply_scaffold(
|
|
80
|
+
path: str | Path,
|
|
81
|
+
*,
|
|
82
|
+
profiles: list[str] | None = None,
|
|
83
|
+
include_claims: bool = False,
|
|
84
|
+
include_evidence: bool = False,
|
|
85
|
+
) -> dict[str, object]:
|
|
86
|
+
plan = plan_scaffold(path, profiles=profiles, include_claims=include_claims, include_evidence=include_evidence)
|
|
87
|
+
catalog = build_catalog_slice(profiles)
|
|
88
|
+
repo_root = Path(plan["repo_root"])
|
|
89
|
+
|
|
90
|
+
created = {"features": [], "claims": [], "tests": [], "evidence": []}
|
|
91
|
+
unchanged = {"features": [], "claims": [], "tests": [], "evidence": []}
|
|
92
|
+
|
|
93
|
+
missing = {key: set(value) for key, value in plan["missing"].items()}
|
|
94
|
+
creatable_tests = set(plan["creatable_test_ids"])
|
|
95
|
+
|
|
96
|
+
claim_rows = {row["id"]: row for row in catalog["claims"]}
|
|
97
|
+
evidence_rows = {row["id"]: row for row in catalog["evidence"]}
|
|
98
|
+
feature_rows = {row["id"]: row for row in catalog["features"]}
|
|
99
|
+
test_rows = {row["id"]: row for row in catalog["tests"]}
|
|
100
|
+
|
|
101
|
+
for feature_id in missing["features"]:
|
|
102
|
+
row = deepcopy(feature_rows[feature_id])
|
|
103
|
+
row["spec_ids"] = []
|
|
104
|
+
row["claim_ids"] = []
|
|
105
|
+
row["test_ids"] = []
|
|
106
|
+
create_entity(path, "features", row)
|
|
107
|
+
created["features"].append(feature_id)
|
|
108
|
+
|
|
109
|
+
if include_claims:
|
|
110
|
+
for claim_id in missing["claims"]:
|
|
111
|
+
row = deepcopy(claim_rows[claim_id])
|
|
112
|
+
row["test_ids"] = []
|
|
113
|
+
row["evidence_ids"] = []
|
|
114
|
+
create_entity(path, "claims", row)
|
|
115
|
+
created["claims"].append(claim_id)
|
|
116
|
+
else:
|
|
117
|
+
unchanged["claims"].extend(sorted(plan["missing"]["claims"]))
|
|
118
|
+
|
|
119
|
+
if include_evidence:
|
|
120
|
+
for evidence_id in missing["evidence"]:
|
|
121
|
+
row = deepcopy(evidence_rows[evidence_id])
|
|
122
|
+
target = repo_root / str(row["path"])
|
|
123
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
if not target.exists():
|
|
125
|
+
target.write_text("{}", encoding="utf-8")
|
|
126
|
+
row["test_ids"] = []
|
|
127
|
+
create_entity(path, "evidence", row)
|
|
128
|
+
created["evidence"].append(evidence_id)
|
|
129
|
+
|
|
130
|
+
for test_id in creatable_tests:
|
|
131
|
+
if test_id not in missing["tests"]:
|
|
132
|
+
continue
|
|
133
|
+
row = deepcopy(test_rows[test_id])
|
|
134
|
+
target = repo_root / str(row["path"])
|
|
135
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
if not target.exists():
|
|
137
|
+
target.write_text("def test_placeholder():\n assert True\n", encoding="utf-8")
|
|
138
|
+
create_entity(path, "tests", row)
|
|
139
|
+
created["tests"].append(test_id)
|
|
140
|
+
|
|
141
|
+
validation = validate_registry(path)
|
|
142
|
+
return {
|
|
143
|
+
"passed": validation["passed"],
|
|
144
|
+
"profiles": plan["profiles"],
|
|
145
|
+
"families": plan["families"],
|
|
146
|
+
"created": created,
|
|
147
|
+
"unchanged": unchanged,
|
|
148
|
+
"blocked_tests": plan["blocked_tests"],
|
|
149
|
+
"validation": validation,
|
|
150
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ssot-conformance
|
|
3
|
+
Version: 0.2.15
|
|
4
|
+
Summary: Reusable SSOT conformance harness, pytest plugin, and scaffold helpers.
|
|
5
|
+
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/groupsum/ssot-registry/tree/main/pkgs/ssot-conformance
|
|
8
|
+
Project-URL: Repository, https://github.com/groupsum/ssot-registry/tree/main/pkgs/ssot-conformance
|
|
9
|
+
Project-URL: Issues, https://github.com/groupsum/ssot-registry/issues
|
|
10
|
+
Keywords: ssot,conformance,pytest,governance,validation,scaffold
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Plugins
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: <3.14,>=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: ssot-core==0.2.15
|
|
29
|
+
Requires-Dist: ssot-contracts==0.2.15
|
|
30
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
<h1>🔷 ssot-conformance</h1>
|
|
34
|
+
<p><strong>Reusable SSOT conformance harness for downstream repositories.</strong></p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div align="center">
|
|
38
|
+
<a href="https://pypi.org/project/ssot-conformance/"><img src="https://img.shields.io/pypi/v/ssot-conformance?label=PyPI%20version" alt="PyPI version" /></a>
|
|
39
|
+
<a href="https://pypi.org/project/ssot-conformance/"><img src="https://img.shields.io/pypi/pyversions/ssot-conformance?label=Python" alt="Supported Python versions" /></a>
|
|
40
|
+
<a href="https://pepy.tech/project/ssot-conformance"><img src="https://static.pepy.tech/badge/ssot-conformance" alt="Downloads" /></a>
|
|
41
|
+
<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="Repository hits" /></a>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
Reusable SSOT conformance harness for downstream repositories.
|
|
45
|
+
|
|
46
|
+
This package provides:
|
|
47
|
+
|
|
48
|
+
- portable conformance case families grouped by registry, document, id, SPEC-to-ADR, feature-to-SPEC, proof-chain, and boundary/release concerns
|
|
49
|
+
- a `pytest` plugin entry point for repo-root selection, case-family filtering, and evidence-output emission
|
|
50
|
+
- scaffold helpers that can compute and optionally create missing conformance SSOT rows
|
|
51
|
+
- machine-readable evidence output suitable for later SSOT evidence ingestion and status synchronization
|
|
52
|
+
|
|
53
|
+
The package is intentionally repo-agnostic. It evaluates a target repository through `.ssot` artifacts and registry semantics rather than assuming a specific implementation language for the target system.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install ssot-conformance
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For local development from this repository:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e pkgs/ssot-conformance
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Execution model
|
|
68
|
+
|
|
69
|
+
Packaged `ssot-core` conformance remains pytest-based, but the operator-facing execution model is registry-driven:
|
|
70
|
+
|
|
71
|
+
- packaged conformance rows store executable `tests[].execution` metadata
|
|
72
|
+
- `ssot conformance run` resolves governed conformance tests and runs those stored commands
|
|
73
|
+
- downstream repos can use the same `tests[].execution` contract for arbitrary command-backed suites
|
|
74
|
+
|
|
75
|
+
For direct SSOT entity execution, prefer:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
ssot test run . --id tst:pytest.conformance.registry-contract
|
|
79
|
+
ssot spec run-tests . --id spc:0525
|
|
80
|
+
ssot boundary run-tests . --id bnd:full-cert
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Package relationships
|
|
84
|
+
|
|
85
|
+
- Package type: reusable conformance and pytest plugin package
|
|
86
|
+
- Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
|
|
87
|
+
- Consumed by: [ssot-cli](https://pypi.org/project/ssot-cli/) and downstream repositories that run packaged SSOT conformance checks
|
|
88
|
+
|
|
89
|
+
If you need the CLI wrapper for these checks, install [ssot-cli](https://pypi.org/project/ssot-cli/). If you need to embed or run the reusable conformance cases directly, this is the package to install.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/ssot_conformance/__init__.py
|
|
4
|
+
src/ssot_conformance/catalog.py
|
|
5
|
+
src/ssot_conformance/discovery.py
|
|
6
|
+
src/ssot_conformance/evidence.py
|
|
7
|
+
src/ssot_conformance/plugin.py
|
|
8
|
+
src/ssot_conformance/runner.py
|
|
9
|
+
src/ssot_conformance/scaffold.py
|
|
10
|
+
src/ssot_conformance.egg-info/PKG-INFO
|
|
11
|
+
src/ssot_conformance.egg-info/SOURCES.txt
|
|
12
|
+
src/ssot_conformance.egg-info/dependency_links.txt
|
|
13
|
+
src/ssot_conformance.egg-info/entry_points.txt
|
|
14
|
+
src/ssot_conformance.egg-info/requires.txt
|
|
15
|
+
src/ssot_conformance.egg-info/top_level.txt
|
|
16
|
+
src/ssot_conformance/cases/__init__.py
|
|
17
|
+
src/ssot_conformance/cases/test_boundary_release_contract.py
|
|
18
|
+
src/ssot_conformance/cases/test_document_contract.py
|
|
19
|
+
src/ssot_conformance/cases/test_feature_spec_contract.py
|
|
20
|
+
src/ssot_conformance/cases/test_id_contract.py
|
|
21
|
+
src/ssot_conformance/cases/test_proof_chain_contract.py
|
|
22
|
+
src/ssot_conformance/cases/test_registry_contract.py
|
|
23
|
+
src/ssot_conformance/cases/test_spec_adr_contract.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ssot_conformance
|