ssot-cli 0.1.9.dev1__tar.gz → 0.1.9.dev3__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.
Files changed (31) hide show
  1. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/PKG-INFO +22 -6
  2. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/README.md +18 -3
  3. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/pyproject.toml +4 -3
  4. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/boundary_cmd.py +21 -0
  5. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/common.py +25 -0
  6. ssot_cli-0.1.9.dev3/src/ssot_cli/conformance_cmd.py +88 -0
  7. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/main.py +2 -0
  8. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/release_cmd.py +42 -3
  9. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/spec_cmd.py +21 -0
  10. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/test_cmd.py +41 -2
  11. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli.egg-info/PKG-INFO +22 -6
  12. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli.egg-info/SOURCES.txt +1 -0
  13. ssot_cli-0.1.9.dev3/src/ssot_cli.egg-info/requires.txt +6 -0
  14. ssot_cli-0.1.9.dev1/src/ssot_cli.egg-info/requires.txt +0 -5
  15. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/setup.cfg +0 -0
  16. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/__init__.py +0 -0
  17. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/adr_cmd.py +0 -0
  18. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/claim_cmd.py +0 -0
  19. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/evidence_cmd.py +0 -0
  20. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/feature_cmd.py +0 -0
  21. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/graph_cmd.py +0 -0
  22. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/init_cmd.py +0 -0
  23. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/issue_cmd.py +0 -0
  24. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/profile_cmd.py +0 -0
  25. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/registry_cmd.py +0 -0
  26. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/risk_cmd.py +0 -0
  27. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/upgrade_cmd.py +0 -0
  28. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli/validate_cmd.py +0 -0
  29. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli.egg-info/dependency_links.txt +0 -0
  30. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli.egg-info/entry_points.txt +0 -0
  31. {ssot_cli-0.1.9.dev1 → ssot_cli-0.1.9.dev3}/src/ssot_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssot-cli
3
- Version: 0.1.9.dev1
3
+ Version: 0.1.9.dev3
4
4
  Summary: Primary CLI distribution for ssot-registry.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License-Expression: Apache-2.0
@@ -24,8 +24,9 @@ Classifier: Topic :: Software Development :: Quality Assurance
24
24
  Classifier: Topic :: Utilities
25
25
  Requires-Python: <3.14,>=3.10
26
26
  Description-Content-Type: text/markdown
27
- Requires-Dist: ssot-contracts<0.3.0,>=0.2.14.dev1
28
- Requires-Dist: ssot-core<0.3.0,>=0.2.14.dev1
27
+ Requires-Dist: ssot-contracts<0.3.0,>=0.2.15.dev1
28
+ Requires-Dist: ssot-core<0.3.0,>=0.2.15.dev1
29
+ Requires-Dist: ssot-conformance<0.3.0,>=0.2.15.dev1
29
30
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
30
31
 
31
32
  <div align="center">
@@ -42,7 +43,7 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
42
43
 
43
44
  `ssot-cli` is the primary command-line distribution for SSOT.
44
45
 
45
- It installs `ssot`, `ssot-cli`, and `ssot-registry` as equivalent executables over the same parser and runtime. The command surface is implemented here, while domain logic lives in [ssot-core](https://pypi.org/project/ssot-core/) and shared contract metadata comes from [ssot-contracts](https://pypi.org/project/ssot-contracts/).
46
+ It installs `ssot`, `ssot-cli`, and `ssot-registry` as equivalent executables over the same parser and runtime. The command surface is implemented here, while domain logic lives in [ssot-core](https://pypi.org/project/ssot-core/), reusable conformance checks come from [ssot-conformance](https://pypi.org/project/ssot-conformance/), and shared contract metadata comes from [ssot-contracts](https://pypi.org/project/ssot-contracts/).
46
47
 
47
48
  - GitHub: https://github.com/groupsum/ssot-registry
48
49
 
@@ -64,7 +65,7 @@ For local development:
64
65
  python -m pip install -e pkgs/ssot-cli
65
66
  ```
66
67
 
67
- This package depends on [ssot-core](https://pypi.org/project/ssot-core/) and [ssot-contracts](https://pypi.org/project/ssot-contracts/), so installing it gives you the full CLI runtime stack.
68
+ This package depends on [ssot-core](https://pypi.org/project/ssot-core/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), and [ssot-contracts](https://pypi.org/project/ssot-contracts/), so installing it gives you the full CLI runtime stack.
68
69
 
69
70
  ## Executable names
70
71
 
@@ -141,6 +142,21 @@ Boundary command help:
141
142
 
142
143
  - Non-zero exit code indicates an operation failure or failed checks.
143
144
 
145
+ ## Registry-driven test execution
146
+
147
+ Tests are the executable SSOT entities. Specs and boundaries resolve to tests through registry links, and runnable tests carry an `execution` object that stores the command, cwd, environment, timeout, and success rule.
148
+
149
+ Examples:
150
+
151
+ ```bash
152
+ ssot test run . --id tst:pytest.conformance.registry-contract
153
+ ssot spec run-tests . --id spc:0525
154
+ ssot boundary run-tests . --id bnd:full-cert
155
+ ssot conformance run . --profiles registry
156
+ ```
157
+
158
+ `ssot conformance run` remains a compatibility wrapper for packaged conformance families. The primary operator story is now registry-driven execution through `test`, `spec`, and `boundary`.
159
+
144
160
  ## Command surface
145
161
 
146
162
  ### Top-level commands
@@ -848,7 +864,7 @@ ssot release certify . --release-id rel:0.1.0 --write-report
848
864
  ## Package relationships
849
865
 
850
866
  - Package type: CLI distribution
851
- - Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
867
+ - Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
852
868
  - Related packages: [ssot-registry](https://pypi.org/project/ssot-registry/), [ssot-tui](https://pypi.org/project/ssot-tui/), [ssot-views](https://pypi.org/project/ssot-views/), [ssot-codegen](https://pypi.org/project/ssot-codegen/)
853
869
 
854
870
  If you need the command-line interface, this is the package to install.
@@ -12,7 +12,7 @@
12
12
 
13
13
  `ssot-cli` is the primary command-line distribution for SSOT.
14
14
 
15
- It installs `ssot`, `ssot-cli`, and `ssot-registry` as equivalent executables over the same parser and runtime. The command surface is implemented here, while domain logic lives in [ssot-core](https://pypi.org/project/ssot-core/) and shared contract metadata comes from [ssot-contracts](https://pypi.org/project/ssot-contracts/).
15
+ It installs `ssot`, `ssot-cli`, and `ssot-registry` as equivalent executables over the same parser and runtime. The command surface is implemented here, while domain logic lives in [ssot-core](https://pypi.org/project/ssot-core/), reusable conformance checks come from [ssot-conformance](https://pypi.org/project/ssot-conformance/), and shared contract metadata comes from [ssot-contracts](https://pypi.org/project/ssot-contracts/).
16
16
 
17
17
  - GitHub: https://github.com/groupsum/ssot-registry
18
18
 
@@ -34,7 +34,7 @@ For local development:
34
34
  python -m pip install -e pkgs/ssot-cli
35
35
  ```
36
36
 
37
- This package depends on [ssot-core](https://pypi.org/project/ssot-core/) and [ssot-contracts](https://pypi.org/project/ssot-contracts/), so installing it gives you the full CLI runtime stack.
37
+ This package depends on [ssot-core](https://pypi.org/project/ssot-core/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), and [ssot-contracts](https://pypi.org/project/ssot-contracts/), so installing it gives you the full CLI runtime stack.
38
38
 
39
39
  ## Executable names
40
40
 
@@ -111,6 +111,21 @@ Boundary command help:
111
111
 
112
112
  - Non-zero exit code indicates an operation failure or failed checks.
113
113
 
114
+ ## Registry-driven test execution
115
+
116
+ Tests are the executable SSOT entities. Specs and boundaries resolve to tests through registry links, and runnable tests carry an `execution` object that stores the command, cwd, environment, timeout, and success rule.
117
+
118
+ Examples:
119
+
120
+ ```bash
121
+ ssot test run . --id tst:pytest.conformance.registry-contract
122
+ ssot spec run-tests . --id spc:0525
123
+ ssot boundary run-tests . --id bnd:full-cert
124
+ ssot conformance run . --profiles registry
125
+ ```
126
+
127
+ `ssot conformance run` remains a compatibility wrapper for packaged conformance families. The primary operator story is now registry-driven execution through `test`, `spec`, and `boundary`.
128
+
114
129
  ## Command surface
115
130
 
116
131
  ### Top-level commands
@@ -818,7 +833,7 @@ ssot release certify . --release-id rel:0.1.0 --write-report
818
833
  ## Package relationships
819
834
 
820
835
  - Package type: CLI distribution
821
- - Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
836
+ - Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
822
837
  - Related packages: [ssot-registry](https://pypi.org/project/ssot-registry/), [ssot-tui](https://pypi.org/project/ssot-tui/), [ssot-views](https://pypi.org/project/ssot-views/), [ssot-codegen](https://pypi.org/project/ssot-codegen/)
823
838
 
824
839
  If you need the command-line interface, this is the package to install.
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ssot-cli"
7
- version = "0.1.9.dev1"
7
+ version = "0.1.9.dev3"
8
8
  description = "Primary CLI distribution for ssot-registry."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<3.14"
11
11
  license = "Apache-2.0"
12
12
  authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
13
13
  dependencies = [
14
- "ssot-contracts>=0.2.14.dev1,<0.3.0",
15
- "ssot-core>=0.2.14.dev1,<0.3.0",
14
+ "ssot-contracts>=0.2.15.dev1,<0.3.0",
15
+ "ssot-core>=0.2.15.dev1,<0.3.0",
16
+ "ssot-conformance>=0.2.15.dev1,<0.3.0",
16
17
  "tomli>=2.0.1; python_version < '3.11'",
17
18
  ]
18
19
  keywords = ["ssot", "cli", "registry", "governance", "release-management", "validation", "developer-tools"]
@@ -12,6 +12,7 @@ from ssot_registry.api import (
12
12
  list_entities,
13
13
  remove_boundary_features,
14
14
  remove_boundary_profiles,
15
+ run_boundary_tests,
15
16
  update_entity,
16
17
  )
17
18
  from ssot_cli.common import add_ids_argument, add_optional_bool_argument, add_path_argument, compact_dict
@@ -87,6 +88,17 @@ def register_boundary(subparsers: argparse._SubParsersAction) -> None:
87
88
  freeze.add_argument("--boundary-id", default=None, help="Boundary id to freeze. Omit to freeze the active boundary.")
88
89
  freeze.set_defaults(func=run_freeze)
89
90
 
91
+ run_tests = boundary_sub.add_parser(
92
+ "run-tests",
93
+ help="Resolve and execute tests for a boundary.",
94
+ description="Resolve the boundary's direct and profile-expanded features, then execute their linked test rows from registry metadata.",
95
+ )
96
+ add_path_argument(run_tests)
97
+ run_tests.add_argument("--id", default=None, help="Boundary id to execute. Omit to use program.active_boundary_id.")
98
+ run_tests.add_argument("--dry-run", action="store_true", help="Resolve linked tests without executing them.")
99
+ run_tests.add_argument("--evidence-output", default=None, help="Optional JSON output path for machine-readable execution evidence.")
100
+ run_tests.set_defaults(func=run_execute_tests)
101
+
90
102
 
91
103
  def run_create(args: argparse.Namespace) -> dict[str, object]:
92
104
  row = {
@@ -138,3 +150,12 @@ def run_remove_profile(args: argparse.Namespace) -> dict[str, object]:
138
150
  def run_freeze(args: argparse.Namespace) -> dict[str, object]:
139
151
  return freeze_boundary(path=args.path, boundary_id=args.boundary_id)
140
152
 
153
+
154
+ def run_execute_tests(args: argparse.Namespace) -> dict[str, object]:
155
+ return run_boundary_tests(
156
+ args.path,
157
+ boundary_id=args.id,
158
+ evidence_output=args.evidence_output,
159
+ dry_run=args.dry_run,
160
+ )
161
+
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import json
5
+ from pathlib import Path
4
6
  from typing import Any
5
7
 
6
8
 
@@ -43,3 +45,26 @@ def collect_list_fields(args: argparse.Namespace, mapping: dict[str, str]) -> di
43
45
  if value:
44
46
  links[field_name] = value
45
47
  return links
48
+
49
+
50
+ def load_json_object_argument(
51
+ *,
52
+ inline_value: str | None,
53
+ file_value: str | None,
54
+ label: str,
55
+ ) -> dict[str, Any] | None:
56
+ if inline_value is None and file_value is None:
57
+ return None
58
+ if inline_value is not None and file_value is not None:
59
+ raise ValueError(f"{label} accepts only one of inline JSON or JSON file")
60
+ if file_value is not None:
61
+ raw_text = Path(file_value).read_text(encoding="utf-8")
62
+ else:
63
+ raw_text = inline_value or ""
64
+ try:
65
+ payload = json.loads(raw_text)
66
+ except json.JSONDecodeError as exc:
67
+ raise ValueError(f"{label} must be valid JSON: {exc.msg}") from exc
68
+ if not isinstance(payload, dict):
69
+ raise ValueError(f"{label} must decode to a JSON object")
70
+ return payload
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ssot_registry.api import run_resolved_test_rows
6
+ from ssot_cli.common import add_path_argument
7
+
8
+
9
+ def register_conformance(subparsers: argparse._SubParsersAction) -> None:
10
+ conformance = subparsers.add_parser(
11
+ "conformance",
12
+ help="Conformance package, scaffold, and execution operations.",
13
+ description="Operate reusable SSOT conformance discovery, scaffold, pytest execution, and evidence-output flows.",
14
+ )
15
+ conformance_sub = conformance.add_subparsers(dest="conformance_command", required=True)
16
+
17
+ profile = conformance_sub.add_parser("profile", help="Profile discovery.", description="List available SSOT conformance profiles and their grouped case families.")
18
+ profile_sub = profile.add_subparsers(dest="conformance_profile_command", required=True)
19
+ profile_list = profile_sub.add_parser("list", help="List conformance profiles.", description="Show built-in SSOT conformance profiles and family counts.")
20
+ add_path_argument(profile_list)
21
+ profile_list.set_defaults(func=run_profile_list)
22
+
23
+ discover = conformance_sub.add_parser("discover", help="Discover conformance cases.", description="Resolve selected SSOT conformance profiles into concrete case files and linked SSOT rows.")
24
+ add_path_argument(discover)
25
+ discover.add_argument("--profiles", nargs="*", default=None, help="Conformance profiles or family names to resolve.")
26
+ discover.set_defaults(func=run_discover)
27
+
28
+ scaffold = conformance_sub.add_parser("scaffold", help="Plan or apply conformance rows.", description="Plan or create missing conformance feature, claim, evidence, and test rows in a target SSOT repo.")
29
+ add_path_argument(scaffold)
30
+ scaffold.add_argument("--profiles", nargs="*", default=None, help="Conformance profiles or family names to scaffold.")
31
+ scaffold.add_argument("--apply", action="store_true", help="Create missing rows instead of only reporting the dry-run plan.")
32
+ scaffold.add_argument("--include-claims", action="store_true", help="Allow scaffold apply to create missing claim rows.")
33
+ scaffold.add_argument("--include-evidence", action="store_true", help="Allow scaffold apply to create missing evidence rows and placeholder artifacts.")
34
+ scaffold.set_defaults(func=run_scaffold)
35
+
36
+ run = conformance_sub.add_parser(
37
+ "run",
38
+ help="Run conformance cases from registry metadata.",
39
+ description="Resolve selected SSOT conformance case families into governed test rows and execute the stored registry commands.",
40
+ )
41
+ add_path_argument(run)
42
+ run.add_argument("--profiles", nargs="*", default=None, help="Conformance profiles or family names to execute.")
43
+ run.add_argument("--dry-run", action="store_true", help="Resolve governed conformance tests without executing them.")
44
+ run.add_argument("--evidence-output", default=None, help="Optional JSON output path for machine-readable evidence.")
45
+ run.set_defaults(func=run_run)
46
+
47
+
48
+ def run_profile_list(args: argparse.Namespace) -> dict[str, object]:
49
+ from ssot_conformance import list_profiles
50
+
51
+ return {"passed": True, "profiles": list_profiles()}
52
+
53
+
54
+ def run_discover(args: argparse.Namespace) -> dict[str, object]:
55
+ from ssot_conformance import discover_cases
56
+
57
+ return discover_cases(args.profiles)
58
+
59
+
60
+ def run_scaffold(args: argparse.Namespace) -> dict[str, object]:
61
+ from ssot_conformance import apply_scaffold, plan_scaffold
62
+
63
+ if args.apply:
64
+ return apply_scaffold(
65
+ args.path,
66
+ profiles=args.profiles,
67
+ include_claims=args.include_claims,
68
+ include_evidence=args.include_evidence,
69
+ )
70
+ return plan_scaffold(
71
+ args.path,
72
+ profiles=args.profiles,
73
+ include_claims=args.include_claims,
74
+ include_evidence=args.include_evidence,
75
+ )
76
+
77
+
78
+ def run_run(args: argparse.Namespace) -> dict[str, object]:
79
+ from ssot_conformance import build_catalog_slice
80
+
81
+ catalog = build_catalog_slice(args.profiles)
82
+ return run_resolved_test_rows(
83
+ args.path,
84
+ target={"kind": "conformance", "profiles": catalog["profiles"], "families": catalog["families"]},
85
+ tests=list(catalog["tests"]),
86
+ evidence_output=args.evidence_output,
87
+ dry_run=args.dry_run,
88
+ )
@@ -12,6 +12,7 @@ from ssot_registry.util.formatting import render_payload
12
12
  from .adr_cmd import register_adr
13
13
  from .boundary_cmd import register_boundary
14
14
  from .claim_cmd import register_claim
15
+ from .conformance_cmd import register_conformance
15
16
  from .evidence_cmd import register_evidence
16
17
  from .feature_cmd import register_feature
17
18
  from .graph_cmd import register_graph
@@ -68,6 +69,7 @@ def build_parser(*, prog: str | None = None) -> argparse.ArgumentParser:
68
69
  register_test(subparsers)
69
70
  register_issue(subparsers)
70
71
  register_claim(subparsers)
72
+ register_conformance(subparsers)
71
73
  register_evidence(subparsers)
72
74
  register_risk(subparsers)
73
75
  register_boundary(subparsers)
@@ -4,6 +4,7 @@ import argparse
4
4
 
5
5
  from ssot_registry.api import (
6
6
  add_release_claims,
7
+ add_release_boundaries,
7
8
  add_release_evidence,
8
9
  certify_release,
9
10
  create_entity,
@@ -13,6 +14,7 @@ from ssot_registry.api import (
13
14
  promote_release,
14
15
  publish_release,
15
16
  remove_release_claims,
17
+ remove_release_boundaries,
16
18
  remove_release_evidence,
17
19
  revoke_release,
18
20
  update_entity,
@@ -33,7 +35,8 @@ def register_release(subparsers: argparse._SubParsersAction) -> None:
33
35
  create.add_argument("--id", required=True, help="Normalized release id to create.")
34
36
  create.add_argument("--version", required=True, help="Semantic or operator-defined version string for the release.")
35
37
  create.add_argument("--status", choices=["draft", "candidate", "certified", "promoted", "published", "revoked"], default="draft", help="Current publication stage of the release.")
36
- create.add_argument("--boundary-id", required=True, help="Frozen boundary id that defines the release scope.")
38
+ create.add_argument("--boundary-id", required=True, help="Primary frozen boundary id that defines the release scope.")
39
+ create.add_argument("--boundary-ids", nargs="*", default=None, help="Additional frozen boundary ids to include in the release scope. The primary --boundary-id is always included.")
37
40
  create.add_argument("--claim-ids", nargs="*", default=[], help="Claim ids bundled into the release.")
38
41
  create.add_argument("--evidence-ids", nargs="*", default=[], help="Evidence ids bundled into the release.")
39
42
  create.set_defaults(func=run_create)
@@ -53,7 +56,8 @@ def register_release(subparsers: argparse._SubParsersAction) -> None:
53
56
  update.add_argument("--id", required=True, help="Release id to update.")
54
57
  update.add_argument("--version", default=None, help="Replacement release version string.")
55
58
  update.add_argument("--status", choices=["draft", "candidate", "certified", "promoted", "published", "revoked"], default=None, help="Updated publication stage.")
56
- update.add_argument("--boundary-id", default=None, help="Replacement boundary id that defines the release scope.")
59
+ update.add_argument("--boundary-id", default=None, help="Replacement primary boundary id that defines the release scope.")
60
+ update.add_argument("--boundary-ids", nargs="*", default=None, help="Replacement full boundary-id list for the release scope.")
57
61
  update.set_defaults(func=run_update)
58
62
 
59
63
  delete = release_sub.add_parser("delete", help="Delete a release.", description="Remove a release record from the registry.")
@@ -85,6 +89,18 @@ def register_release(subparsers: argparse._SubParsersAction) -> None:
85
89
  remove_evidence.add_argument("--evidence-ids", nargs="+", required=True, help="Evidence ids to remove from the release.")
86
90
  remove_evidence.set_defaults(func=run_remove_evidence)
87
91
 
92
+ add_boundary = release_sub.add_parser("add-boundary", help="Attach boundaries to a release.", description="Add one or more boundary ids to the release scope.")
93
+ add_path_argument(add_boundary)
94
+ add_boundary.add_argument("--id", required=True, help="Release id that should receive the boundaries.")
95
+ add_boundary.add_argument("--boundary-ids", nargs="+", required=True, help="Boundary ids to attach to the release.")
96
+ add_boundary.set_defaults(func=run_add_boundary)
97
+
98
+ remove_boundary = release_sub.add_parser("remove-boundary", help="Remove boundaries from a release.", description="Remove one or more boundary ids from the release scope.")
99
+ add_path_argument(remove_boundary)
100
+ remove_boundary.add_argument("--id", required=True, help="Release id whose boundaries should be removed.")
101
+ remove_boundary.add_argument("--boundary-ids", nargs="+", required=True, help="Boundary ids to remove from the release.")
102
+ remove_boundary.set_defaults(func=run_remove_boundary)
103
+
88
104
  certify = release_sub.add_parser("certify", help="Run release certification.", description="Evaluate release guards and certify the release when all required checks pass.")
89
105
  add_path_argument(certify)
90
106
  certify.add_argument("--release-id", default=None, help="Release id to certify. Omit to use the active release.")
@@ -109,11 +125,14 @@ def register_release(subparsers: argparse._SubParsersAction) -> None:
109
125
 
110
126
 
111
127
  def run_create(args: argparse.Namespace) -> dict[str, object]:
128
+ boundary_ids = [args.boundary_id, *(args.boundary_ids or [])]
129
+ boundary_ids = list(dict.fromkeys(boundary_ids))
112
130
  row = {
113
131
  "id": args.id,
114
132
  "version": args.version,
115
133
  "status": args.status,
116
134
  "boundary_id": args.boundary_id,
135
+ "boundary_ids": boundary_ids,
117
136
  "claim_ids": args.claim_ids,
118
137
  "evidence_ids": args.evidence_ids,
119
138
  }
@@ -129,7 +148,19 @@ def run_list(args: argparse.Namespace) -> dict[str, object]:
129
148
 
130
149
 
131
150
  def run_update(args: argparse.Namespace) -> dict[str, object]:
132
- changes = compact_dict({"version": args.version, "status": args.status, "boundary_id": args.boundary_id})
151
+ boundary_ids = args.boundary_ids
152
+ if boundary_ids is not None:
153
+ boundary_ids = list(dict.fromkeys(([args.boundary_id] if args.boundary_id else []) + boundary_ids))
154
+ if not boundary_ids:
155
+ raise ValueError("At least one boundary id is required when updating boundary membership")
156
+ elif args.boundary_id is not None:
157
+ boundary_ids = [args.boundary_id]
158
+ changes = compact_dict({
159
+ "version": args.version,
160
+ "status": args.status,
161
+ "boundary_id": args.boundary_id or (boundary_ids[0] if boundary_ids else None),
162
+ "boundary_ids": boundary_ids,
163
+ })
133
164
  if not changes:
134
165
  raise ValueError("At least one update field is required")
135
166
  return update_entity(args.path, "releases", args.id, changes)
@@ -155,6 +186,14 @@ def run_remove_evidence(args: argparse.Namespace) -> dict[str, object]:
155
186
  return remove_release_evidence(args.path, args.id, args.evidence_ids)
156
187
 
157
188
 
189
+ def run_add_boundary(args: argparse.Namespace) -> dict[str, object]:
190
+ return add_release_boundaries(args.path, args.id, args.boundary_ids)
191
+
192
+
193
+ def run_remove_boundary(args: argparse.Namespace) -> dict[str, object]:
194
+ return remove_release_boundaries(args.path, args.id, args.boundary_ids)
195
+
196
+
158
197
  def run_certify(args: argparse.Namespace) -> dict[str, object]:
159
198
  return certify_release(path=args.path, release_id=args.release_id, write_report=args.write_report)
160
199
 
@@ -15,6 +15,7 @@ from ssot_registry.api import (
15
15
  sync_documents,
16
16
  supersede_documents,
17
17
  update_document,
18
+ run_spec_tests,
18
19
  )
19
20
  from ssot_cli.common import add_ids_argument, add_path_argument, compact_dict
20
21
 
@@ -116,6 +117,17 @@ def register_spec(subparsers: argparse._SubParsersAction) -> None:
116
117
  add_path_argument(reserve_list)
117
118
  reserve_list.set_defaults(func=run_reserve_list)
118
119
 
120
+ run_tests = spec_sub.add_parser(
121
+ "run-tests",
122
+ help="Resolve and execute tests for a SPEC.",
123
+ description="Resolve features that declare the selected SPEC in feature.spec_ids, then execute their linked test rows from registry metadata.",
124
+ )
125
+ add_path_argument(run_tests)
126
+ run_tests.add_argument("--id", required=True, help="SPEC id whose linked tests should be resolved and run.")
127
+ run_tests.add_argument("--dry-run", action="store_true", help="Resolve linked tests without executing them.")
128
+ run_tests.add_argument("--evidence-output", default=None, help="Optional JSON output path for machine-readable execution evidence.")
129
+ run_tests.set_defaults(func=run_spec_tests_command)
130
+
119
131
 
120
132
  def run_create(args: argparse.Namespace) -> dict[str, object]:
121
133
  return create_document(
@@ -191,3 +203,12 @@ def run_reserve_create(args: argparse.Namespace) -> dict[str, object]:
191
203
  def run_reserve_list(args: argparse.Namespace) -> dict[str, object]:
192
204
  return list_document_reservations(args.path, "spec")
193
205
 
206
+
207
+ def run_spec_tests_command(args: argparse.Namespace) -> dict[str, object]:
208
+ return run_spec_tests(
209
+ args.path,
210
+ spec_id=args.id,
211
+ evidence_output=args.evidence_output,
212
+ dry_run=args.dry_run,
213
+ )
214
+
@@ -2,8 +2,8 @@
2
2
 
3
3
  import argparse
4
4
 
5
- from ssot_registry.api import create_entity, delete_entity, get_entity, link_entities, list_entities, unlink_entities, update_entity
6
- from ssot_cli.common import add_ids_argument, add_path_argument, collect_list_fields, compact_dict
5
+ from ssot_registry.api import create_entity, delete_entity, get_entity, link_entities, list_entities, run_tests, unlink_entities, update_entity
6
+ from ssot_cli.common import add_ids_argument, add_path_argument, collect_list_fields, compact_dict, load_json_object_argument
7
7
 
8
8
 
9
9
  _LINK_MAPPING = {
@@ -31,6 +31,8 @@ def register_test(subparsers: argparse._SubParsersAction) -> None:
31
31
  create.add_argument("--feature-ids", nargs="*", default=[], help="Feature ids this test verifies.")
32
32
  create.add_argument("--claim-ids", nargs="*", default=[], help="Claim ids this test supports.")
33
33
  create.add_argument("--evidence-ids", nargs="*", default=[], help="Evidence ids produced by or associated with the test.")
34
+ create.add_argument("--execution-json", default=None, help="Inline JSON object describing registry-owned execution metadata for this test.")
35
+ create.add_argument("--execution-file", default=None, help="Path to a JSON file containing registry-owned execution metadata for this test.")
34
36
  create.set_defaults(func=run_create)
35
37
 
36
38
  get = test_sub.add_parser("get", help="Show one test.", description="Fetch a single test record by id.")
@@ -50,6 +52,8 @@ def register_test(subparsers: argparse._SubParsersAction) -> None:
50
52
  update.add_argument("--status", choices=["planned", "passing", "failing", "blocked", "skipped"], default=None, help="Updated execution or readiness state.")
51
53
  update.add_argument("--kind", default=None, help="Updated test category.")
52
54
  update.add_argument("--test-path", dest="test_path", default=None, help="Updated repository-relative path to the test or procedure.")
55
+ update.add_argument("--execution-json", default=None, help="Replacement inline JSON object for test execution metadata.")
56
+ update.add_argument("--execution-file", default=None, help="Replacement JSON file for test execution metadata.")
53
57
  update.set_defaults(func=run_update)
54
58
 
55
59
  delete = test_sub.add_parser("delete", help="Delete a test.", description="Remove a test record from the registry.")
@@ -73,6 +77,19 @@ def register_test(subparsers: argparse._SubParsersAction) -> None:
73
77
  unlink.add_argument("--evidence-ids", nargs="*", help="Evidence ids to detach.")
74
78
  unlink.set_defaults(func=run_unlink)
75
79
 
80
+ run = test_sub.add_parser(
81
+ "run",
82
+ help="Execute runnable tests from registry metadata.",
83
+ description="Resolve one or more test rows by id, require execution metadata, and run the stored commands from registry truth.",
84
+ )
85
+ add_path_argument(run)
86
+ selector = run.add_mutually_exclusive_group(required=True)
87
+ selector.add_argument("--id", default=None, help="Single test id to execute.")
88
+ selector.add_argument("--ids", nargs="+", default=None, help="One or more test ids to execute.")
89
+ run.add_argument("--dry-run", action="store_true", help="Resolve tests without executing their commands.")
90
+ run.add_argument("--evidence-output", default=None, help="Optional JSON output path for machine-readable execution evidence.")
91
+ run.set_defaults(func=run_execute)
92
+
76
93
 
77
94
  def _build_links(args: argparse.Namespace) -> dict[str, list[str]]:
78
95
  links = collect_list_fields(args, _LINK_MAPPING)
@@ -82,6 +99,11 @@ def _build_links(args: argparse.Namespace) -> dict[str, list[str]]:
82
99
 
83
100
 
84
101
  def run_create(args: argparse.Namespace) -> dict[str, object]:
102
+ execution = load_json_object_argument(
103
+ inline_value=args.execution_json,
104
+ file_value=args.execution_file,
105
+ label="test execution metadata",
106
+ )
85
107
  row = {
86
108
  "id": args.id,
87
109
  "title": args.title,
@@ -91,6 +113,7 @@ def run_create(args: argparse.Namespace) -> dict[str, object]:
91
113
  "feature_ids": args.feature_ids,
92
114
  "claim_ids": args.claim_ids,
93
115
  "evidence_ids": args.evidence_ids,
116
+ "execution": execution,
94
117
  }
95
118
  return create_entity(args.path, "tests", row)
96
119
 
@@ -104,12 +127,18 @@ def run_list(args: argparse.Namespace) -> dict[str, object]:
104
127
 
105
128
 
106
129
  def run_update(args: argparse.Namespace) -> dict[str, object]:
130
+ execution = load_json_object_argument(
131
+ inline_value=args.execution_json,
132
+ file_value=args.execution_file,
133
+ label="test execution metadata",
134
+ )
107
135
  changes = compact_dict(
108
136
  {
109
137
  "title": args.title,
110
138
  "status": args.status,
111
139
  "kind": args.kind,
112
140
  "path": args.test_path,
141
+ "execution": execution,
113
142
  }
114
143
  )
115
144
  if not changes:
@@ -128,3 +157,13 @@ def run_link(args: argparse.Namespace) -> dict[str, object]:
128
157
  def run_unlink(args: argparse.Namespace) -> dict[str, object]:
129
158
  return unlink_entities(args.path, "tests", args.id, _build_links(args))
130
159
 
160
+
161
+ def run_execute(args: argparse.Namespace) -> dict[str, object]:
162
+ test_ids = [args.id] if args.id is not None else list(args.ids or [])
163
+ return run_tests(
164
+ args.path,
165
+ test_ids=test_ids,
166
+ evidence_output=args.evidence_output,
167
+ dry_run=args.dry_run,
168
+ )
169
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssot-cli
3
- Version: 0.1.9.dev1
3
+ Version: 0.1.9.dev3
4
4
  Summary: Primary CLI distribution for ssot-registry.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License-Expression: Apache-2.0
@@ -24,8 +24,9 @@ Classifier: Topic :: Software Development :: Quality Assurance
24
24
  Classifier: Topic :: Utilities
25
25
  Requires-Python: <3.14,>=3.10
26
26
  Description-Content-Type: text/markdown
27
- Requires-Dist: ssot-contracts<0.3.0,>=0.2.14.dev1
28
- Requires-Dist: ssot-core<0.3.0,>=0.2.14.dev1
27
+ Requires-Dist: ssot-contracts<0.3.0,>=0.2.15.dev1
28
+ Requires-Dist: ssot-core<0.3.0,>=0.2.15.dev1
29
+ Requires-Dist: ssot-conformance<0.3.0,>=0.2.15.dev1
29
30
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
30
31
 
31
32
  <div align="center">
@@ -42,7 +43,7 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
42
43
 
43
44
  `ssot-cli` is the primary command-line distribution for SSOT.
44
45
 
45
- It installs `ssot`, `ssot-cli`, and `ssot-registry` as equivalent executables over the same parser and runtime. The command surface is implemented here, while domain logic lives in [ssot-core](https://pypi.org/project/ssot-core/) and shared contract metadata comes from [ssot-contracts](https://pypi.org/project/ssot-contracts/).
46
+ It installs `ssot`, `ssot-cli`, and `ssot-registry` as equivalent executables over the same parser and runtime. The command surface is implemented here, while domain logic lives in [ssot-core](https://pypi.org/project/ssot-core/), reusable conformance checks come from [ssot-conformance](https://pypi.org/project/ssot-conformance/), and shared contract metadata comes from [ssot-contracts](https://pypi.org/project/ssot-contracts/).
46
47
 
47
48
  - GitHub: https://github.com/groupsum/ssot-registry
48
49
 
@@ -64,7 +65,7 @@ For local development:
64
65
  python -m pip install -e pkgs/ssot-cli
65
66
  ```
66
67
 
67
- This package depends on [ssot-core](https://pypi.org/project/ssot-core/) and [ssot-contracts](https://pypi.org/project/ssot-contracts/), so installing it gives you the full CLI runtime stack.
68
+ This package depends on [ssot-core](https://pypi.org/project/ssot-core/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), and [ssot-contracts](https://pypi.org/project/ssot-contracts/), so installing it gives you the full CLI runtime stack.
68
69
 
69
70
  ## Executable names
70
71
 
@@ -141,6 +142,21 @@ Boundary command help:
141
142
 
142
143
  - Non-zero exit code indicates an operation failure or failed checks.
143
144
 
145
+ ## Registry-driven test execution
146
+
147
+ Tests are the executable SSOT entities. Specs and boundaries resolve to tests through registry links, and runnable tests carry an `execution` object that stores the command, cwd, environment, timeout, and success rule.
148
+
149
+ Examples:
150
+
151
+ ```bash
152
+ ssot test run . --id tst:pytest.conformance.registry-contract
153
+ ssot spec run-tests . --id spc:0525
154
+ ssot boundary run-tests . --id bnd:full-cert
155
+ ssot conformance run . --profiles registry
156
+ ```
157
+
158
+ `ssot conformance run` remains a compatibility wrapper for packaged conformance families. The primary operator story is now registry-driven execution through `test`, `spec`, and `boundary`.
159
+
144
160
  ## Command surface
145
161
 
146
162
  ### Top-level commands
@@ -848,7 +864,7 @@ ssot release certify . --release-id rel:0.1.0 --write-report
848
864
  ## Package relationships
849
865
 
850
866
  - Package type: CLI distribution
851
- - Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
867
+ - Depends on: [ssot-core](https://pypi.org/project/ssot-core/), [ssot-conformance](https://pypi.org/project/ssot-conformance/), [ssot-contracts](https://pypi.org/project/ssot-contracts/)
852
868
  - Related packages: [ssot-registry](https://pypi.org/project/ssot-registry/), [ssot-tui](https://pypi.org/project/ssot-tui/), [ssot-views](https://pypi.org/project/ssot-views/), [ssot-codegen](https://pypi.org/project/ssot-codegen/)
853
869
 
854
870
  If you need the command-line interface, this is the package to install.
@@ -5,6 +5,7 @@ src/ssot_cli/adr_cmd.py
5
5
  src/ssot_cli/boundary_cmd.py
6
6
  src/ssot_cli/claim_cmd.py
7
7
  src/ssot_cli/common.py
8
+ src/ssot_cli/conformance_cmd.py
8
9
  src/ssot_cli/evidence_cmd.py
9
10
  src/ssot_cli/feature_cmd.py
10
11
  src/ssot_cli/graph_cmd.py
@@ -0,0 +1,6 @@
1
+ ssot-contracts<0.3.0,>=0.2.15.dev1
2
+ ssot-core<0.3.0,>=0.2.15.dev1
3
+ ssot-conformance<0.3.0,>=0.2.15.dev1
4
+
5
+ [:python_version < "3.11"]
6
+ tomli>=2.0.1
@@ -1,5 +0,0 @@
1
- ssot-contracts<0.3.0,>=0.2.14.dev1
2
- ssot-core<0.3.0,>=0.2.14.dev1
3
-
4
- [:python_version < "3.11"]
5
- tomli>=2.0.1
File without changes