spck-conformance 0.1.0__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 (44) hide show
  1. spck_conformance-0.1.0/PKG-INFO +96 -0
  2. spck_conformance-0.1.0/README.md +80 -0
  3. spck_conformance-0.1.0/pyproject.toml +38 -0
  4. spck_conformance-0.1.0/setup.cfg +4 -0
  5. spck_conformance-0.1.0/spck_conformance/__init__.py +2 -0
  6. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/checks/engine.py +157 -0
  7. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/checks/merchant.py +348 -0
  8. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/checks/merchant_checks.py +764 -0
  9. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/checkout-lifecycle.json +918 -0
  10. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/discount-consent-identity.json +627 -0
  11. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/discovery-negotiation.json +627 -0
  12. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/errors-validation-security.json +410 -0
  13. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/fulfillment.json +557 -0
  14. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/order.json +356 -0
  15. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/payment.json +626 -0
  16. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/checkout-lifecycle.json +75 -0
  17. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/discount-consent-identity.json +519 -0
  18. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/discovery-negotiation.json +50 -0
  19. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/errors-validation-security.json +322 -0
  20. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/fulfillment.json +47 -0
  21. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/order.json +280 -0
  22. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/payment.json +518 -0
  23. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/cart.json +47 -0
  24. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/catalog.json +54 -0
  25. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/checkout-lifecycle.json +75 -0
  26. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/discounts-consent.json +52 -0
  27. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/discovery.json +90 -0
  28. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/error-envelope.json +53 -0
  29. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/fulfillment.json +47 -0
  30. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/identity-linking.json +854 -0
  31. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/negotiation-errors.json +62 -0
  32. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/order.json +49 -0
  33. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/payment.json +51 -0
  34. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/signals-attribution-eligibility.json +35 -0
  35. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/signatures.json +547 -0
  36. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/totals.json +23 -0
  37. spck_conformance-0.1.0/spck_conformance/_bundle/conformance/selfcheck/verdict_gate.py +160 -0
  38. spck_conformance-0.1.0/spck_conformance/cli.py +26 -0
  39. spck_conformance-0.1.0/spck_conformance.egg-info/PKG-INFO +96 -0
  40. spck_conformance-0.1.0/spck_conformance.egg-info/SOURCES.txt +42 -0
  41. spck_conformance-0.1.0/spck_conformance.egg-info/dependency_links.txt +1 -0
  42. spck_conformance-0.1.0/spck_conformance.egg-info/entry_points.txt +2 -0
  43. spck_conformance-0.1.0/spck_conformance.egg-info/requires.txt +1 -0
  44. spck_conformance-0.1.0/spck_conformance.egg-info/top_level.txt +1 -0
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: spck-conformance
3
+ Version: 0.1.0
4
+ Summary: Unofficial, capability-adaptive conformance runner for the Universal Commerce Protocol (UCP)
5
+ Author: spck.dev
6
+ License: MIT
7
+ Project-URL: Homepage, https://spck.dev
8
+ Project-URL: Source, https://github.com/vishkaty/ucp-conformance
9
+ Keywords: ucp,universal-commerce-protocol,conformance,agentic-commerce
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: certifi>=2023.7.22
16
+
17
+ # spck-conformance
18
+
19
+ Unofficial, capability-adaptive conformance runner for the **Universal Commerce
20
+ Protocol (UCP)**. Point it at any UCP server and get an honest, capability-scoped
21
+ report — it runs only the checks that apply to what the server declares, and every
22
+ check is kill-rate-validated (proven to catch its own defects) before it ships.
23
+
24
+ > Independent project. Not affiliated with, endorsed by, or a substitute for the
25
+ > official UCP conformance suite. It reports only the checks it actually runs.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install spck-conformance
31
+ ```
32
+
33
+ No third-party dependencies (Python ≥ 3.9, stdlib only).
34
+
35
+ ## Use
36
+
37
+ ```bash
38
+ spck-conformance --server https://api.example.com \
39
+ [--config merchant.json] [--json] [--junit report.xml]
40
+ ```
41
+
42
+ ### Quickstart (30 seconds)
43
+
44
+ ```bash
45
+ # 1. point it at your server — no config needed for the discovery + structure checks
46
+ spck-conformance --server https://api.example.com
47
+
48
+ # 2. scaffold a config tailored to YOUR server's declared capabilities
49
+ spck-conformance --server https://api.example.com --init merchant.json
50
+ # -> fill in the FILL_ME placeholders (a product id, discount code, payment token…)
51
+
52
+ # 3. re-run with the config to unlock the data-dependent checks
53
+ spck-conformance --server https://api.example.com --config merchant.json
54
+ ```
55
+
56
+ On a deviation the report shows **expected (the requirement) vs observed (your actual
57
+ response)** so you can fix it directly, and the footer's **Next steps** tells you how to
58
+ unlock any `not-tested` checks.
59
+
60
+ ### Use in CI (GitHub Action)
61
+
62
+ ```yaml
63
+ # .github/workflows/ucp.yml
64
+ jobs:
65
+ conformance:
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - uses: vishkaty/ucp-conformance@main
69
+ with:
70
+ server: https://api.example.com
71
+ config: merchant.json # optional
72
+ # fail-on-deviation: false # report-only mode
73
+ ```
74
+
75
+ The job fails on any MUST deviation and writes a JUnit report (`ucp-conformance.xml`)
76
+ your CI can display as a test run.
77
+
78
+ - **`--config`** — optional JSON supplying data-dependent inputs (product id, discount
79
+ codes, a succeeding/failing payment, an out-of-stock id). Without it, those checks
80
+ are honestly `not-tested` rather than silently passed.
81
+ - **`--json`** — full machine-readable report; each check cites its normative clause
82
+ (id, verbatim text, spec source).
83
+ - **`--junit FILE`** — JUnit XML for CI (deviation → `<failure>`, not-applicable /
84
+ not-tested → `<skipped>`).
85
+ - **Exit code** — `2` if any MUST deviates, else `0` (partial coverage is not a failure).
86
+
87
+ ## What it checks
88
+
89
+ Discovery + profile structure, checkout lifecycle, idempotency, validation,
90
+ fulfillment, order completion, payment-credential handling, discounts, catalog
91
+ (search/lookup), and cart — scoped to the capabilities the target declares. The
92
+ profile-schema check requires the native `ucp-schema` validator (not shipped in the
93
+ wheel), so it reports `not-tested` here; run it from the source repo for full fidelity.
94
+
95
+ Source, methodology, and the self-validating CI harness:
96
+ <https://github.com/vishkaty/ucp-conformance>.
@@ -0,0 +1,80 @@
1
+ # spck-conformance
2
+
3
+ Unofficial, capability-adaptive conformance runner for the **Universal Commerce
4
+ Protocol (UCP)**. Point it at any UCP server and get an honest, capability-scoped
5
+ report — it runs only the checks that apply to what the server declares, and every
6
+ check is kill-rate-validated (proven to catch its own defects) before it ships.
7
+
8
+ > Independent project. Not affiliated with, endorsed by, or a substitute for the
9
+ > official UCP conformance suite. It reports only the checks it actually runs.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install spck-conformance
15
+ ```
16
+
17
+ No third-party dependencies (Python ≥ 3.9, stdlib only).
18
+
19
+ ## Use
20
+
21
+ ```bash
22
+ spck-conformance --server https://api.example.com \
23
+ [--config merchant.json] [--json] [--junit report.xml]
24
+ ```
25
+
26
+ ### Quickstart (30 seconds)
27
+
28
+ ```bash
29
+ # 1. point it at your server — no config needed for the discovery + structure checks
30
+ spck-conformance --server https://api.example.com
31
+
32
+ # 2. scaffold a config tailored to YOUR server's declared capabilities
33
+ spck-conformance --server https://api.example.com --init merchant.json
34
+ # -> fill in the FILL_ME placeholders (a product id, discount code, payment token…)
35
+
36
+ # 3. re-run with the config to unlock the data-dependent checks
37
+ spck-conformance --server https://api.example.com --config merchant.json
38
+ ```
39
+
40
+ On a deviation the report shows **expected (the requirement) vs observed (your actual
41
+ response)** so you can fix it directly, and the footer's **Next steps** tells you how to
42
+ unlock any `not-tested` checks.
43
+
44
+ ### Use in CI (GitHub Action)
45
+
46
+ ```yaml
47
+ # .github/workflows/ucp.yml
48
+ jobs:
49
+ conformance:
50
+ runs-on: ubuntu-latest
51
+ steps:
52
+ - uses: vishkaty/ucp-conformance@main
53
+ with:
54
+ server: https://api.example.com
55
+ config: merchant.json # optional
56
+ # fail-on-deviation: false # report-only mode
57
+ ```
58
+
59
+ The job fails on any MUST deviation and writes a JUnit report (`ucp-conformance.xml`)
60
+ your CI can display as a test run.
61
+
62
+ - **`--config`** — optional JSON supplying data-dependent inputs (product id, discount
63
+ codes, a succeeding/failing payment, an out-of-stock id). Without it, those checks
64
+ are honestly `not-tested` rather than silently passed.
65
+ - **`--json`** — full machine-readable report; each check cites its normative clause
66
+ (id, verbatim text, spec source).
67
+ - **`--junit FILE`** — JUnit XML for CI (deviation → `<failure>`, not-applicable /
68
+ not-tested → `<skipped>`).
69
+ - **Exit code** — `2` if any MUST deviates, else `0` (partial coverage is not a failure).
70
+
71
+ ## What it checks
72
+
73
+ Discovery + profile structure, checkout lifecycle, idempotency, validation,
74
+ fulfillment, order completion, payment-credential handling, discounts, catalog
75
+ (search/lookup), and cart — scoped to the capabilities the target declares. The
76
+ profile-schema check requires the native `ucp-schema` validator (not shipped in the
77
+ wheel), so it reports `not-tested` here; run it from the source repo for full fidelity.
78
+
79
+ Source, methodology, and the self-validating CI harness:
80
+ <https://github.com/vishkaty/ucp-conformance>.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "spck-conformance"
7
+ version = "0.1.0"
8
+ description = "Unofficial, capability-adaptive conformance runner for the Universal Commerce Protocol (UCP)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "spck.dev"}]
13
+ keywords = ["ucp", "universal-commerce-protocol", "conformance", "agentic-commerce"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ ]
19
+ # certifi provides a CA bundle so TLS verification works on Pythons that ship none
20
+ # (common on macOS python.org builds). Everything else is stdlib.
21
+ dependencies = ["certifi>=2023.7.22"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://spck.dev"
25
+ Source = "https://github.com/vishkaty/ucp-conformance"
26
+
27
+ [project.scripts]
28
+ spck-conformance = "spck_conformance.cli:main"
29
+
30
+ [tool.setuptools]
31
+ packages = ["spck_conformance"]
32
+
33
+ [tool.setuptools.package-data]
34
+ spck_conformance = [
35
+ "_bundle/conformance/checks/*.py",
36
+ "_bundle/conformance/selfcheck/*.py",
37
+ "_bundle/conformance/requirements/*/*.json",
38
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """spck-conformance — unofficial capability-adaptive UCP conformance runner."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ engine.py — Phase 1 register-driven conformance check engine.
4
+
5
+ A check cites the register requirement(s) it verifies, evaluates a real server
6
+ response, and declares the mutations that MUST break it. Before any check counts
7
+ toward a verdict, the engine self-validates it: the check must pass on the clean
8
+ response AND fail on every declared mutant (kill-rate). A check that isn't
9
+ mutation-safe is reported kill_safe=False, which the verdict gate downgrades to
10
+ inconclusive (so it can never produce a green). Results feed verdict_gate for an
11
+ honest, coverage-gated report.
12
+
13
+ Mutations here operate on the CAPTURED response (deterministic golden+mutation),
14
+ which works for stateful checks too — we mutate the final response under test.
15
+ The live mutation_proxy remains for integration/timing/replay cases.
16
+ """
17
+ import json, sys, ssl, urllib.request, urllib.error, pathlib, glob
18
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "selfcheck"))
19
+ from verdict_gate import CheckResult, aggregate, CLEAN, DEVIATION, INCONCLUSIVE # noqa: E402
20
+
21
+ ROOT = pathlib.Path(__file__).resolve().parents[2]
22
+ REQ_DIR = ROOT / "conformance" / "requirements"
23
+
24
+ # ---- TLS: use certifi's CA bundle if available (many Pythons ship no bundle, so a
25
+ # plain urlopen against https fails with CERTIFICATE_VERIFY_FAILED). --insecure skips
26
+ # verification entirely (testing only). -------------------------------------------
27
+ def _make_ssl_context(insecure=False):
28
+ if insecure:
29
+ ctx = ssl.create_default_context()
30
+ ctx.check_hostname = False
31
+ ctx.verify_mode = ssl.CERT_NONE
32
+ return ctx
33
+ try:
34
+ import certifi
35
+ return ssl.create_default_context(cafile=certifi.where())
36
+ except Exception:
37
+ return ssl.create_default_context()
38
+
39
+ INSECURE = False
40
+ _SSL_CTX = _make_ssl_context()
41
+
42
+ def set_insecure(v):
43
+ """Toggle TLS verification (used by the --insecure CLI flag)."""
44
+ global INSECURE, _SSL_CTX
45
+ INSECURE = bool(v)
46
+ _SSL_CTX = _make_ssl_context(INSECURE)
47
+
48
+ class TLSError(Exception):
49
+ """Raised on a TLS certificate verification failure, with a friendly hint."""
50
+
51
+ # ---- response capture -------------------------------------------------------
52
+ class Resp:
53
+ def __init__(self, status, headers, body):
54
+ self.status, self.headers, self.body = status, dict(headers), body
55
+ try: self.json = json.loads(body)
56
+ except Exception: self.json = None
57
+ def clone(self):
58
+ return Resp(self.status, dict(self.headers), self.body)
59
+
60
+ def fetch(base, path, method="GET", body=None, headers=None):
61
+ data = json.dumps(body).encode() if body is not None else None
62
+ h = {"Content-Type": "application/json", **(headers or {})}
63
+ req = urllib.request.Request(base.rstrip("/") + path, data=data, method=method, headers=h)
64
+ try:
65
+ with urllib.request.urlopen(req, context=_SSL_CTX) as r:
66
+ return Resp(r.status, r.getheaders(), r.read())
67
+ except urllib.error.HTTPError as e:
68
+ return Resp(e.code, e.headers.items(), e.read())
69
+ except Exception as e:
70
+ return Resp(0, {}, str(e).encode())
71
+
72
+ # ---- mutations on a captured response (defect injection) --------------------
73
+ def _reparse(r):
74
+ r.body = json.dumps(r.json).encode() if r.json is not None else r.body
75
+ return r
76
+ def mutate(resp, mut):
77
+ r = resp.clone()
78
+ name, _, arg = mut.partition(":")
79
+ if name == "status": r.status = int(arg); return r
80
+ if name == "empty": r.body = b""; r.json = None; return r
81
+ if name == "corrupt-json": r.body = (r.body or b"{}")[:-1]; r.json = None; return r
82
+ if name == "drop" and r.json is not None:
83
+ cur = r.json; parts = arg.split(".")
84
+ for p in parts[:-1]:
85
+ if isinstance(cur, list) and p.isdigit() and int(p) < len(cur): cur = cur[int(p)]
86
+ elif isinstance(cur, dict): cur = cur.get(p)
87
+ else: cur = None
88
+ if cur is None: break
89
+ last = parts[-1]
90
+ if isinstance(cur, dict): cur.pop(last, None)
91
+ elif isinstance(cur, list) and last.isdigit() and int(last) < len(cur): cur.pop(int(last))
92
+ return _reparse(r)
93
+ if name == "set" and r.json is not None:
94
+ path, _, v = arg.partition("="); val = json.loads(v)
95
+ cur = r.json; parts = path.split(".")
96
+ for p in parts[:-1]:
97
+ if isinstance(cur, list) and p.isdigit() and int(p) < len(cur): cur = cur[int(p)]
98
+ elif isinstance(cur, dict): cur = cur.get(p)
99
+ else: cur = None
100
+ if cur is None: break
101
+ last = parts[-1]
102
+ if isinstance(cur, dict): cur[last] = val
103
+ elif isinstance(cur, list) and last.isdigit() and int(last) < len(cur): cur[int(last)] = val
104
+ return _reparse(r)
105
+ if name == "hset": # set/replace a response header (case-insensitive): hset:Content-Type=text/plain
106
+ k, _, v = arg.partition("=")
107
+ for hk in [h for h in r.headers if h.lower() == k.lower()]: r.headers.pop(hk)
108
+ r.headers[k] = v; return r
109
+ if name == "hdrop": # remove a response header (case-insensitive)
110
+ for hk in [h for h in r.headers if h.lower() == arg.lower()]: r.headers.pop(hk)
111
+ return r
112
+ return r # no-op (mutation not applicable)
113
+
114
+ # ---- check spec -------------------------------------------------------------
115
+ class Check:
116
+ def __init__(self, cid, req_ids, keyword, fetch_fn, predicate, mutations):
117
+ self.id, self.req_ids, self.keyword = cid, req_ids, keyword
118
+ self.fetch_fn, self.predicate, self.mutations = fetch_fn, predicate, mutations
119
+
120
+ def run_check(chk, base):
121
+ """Returns (results: list[CheckResult], detail: dict). kill_safe is computed by
122
+ mutation: clean must pass, every declared mutant must deviate."""
123
+ try:
124
+ golden = chk.fetch_fn(base)
125
+ except Exception as e:
126
+ kill_safe = False
127
+ res = [CheckResult(rid, chk.keyword, INCONCLUSIVE, kill_safe) for rid in chk.req_ids]
128
+ return res, {"clean": "error:" + str(e), "kills": "", "kill_safe": kill_safe}
129
+ clean = chk.predicate(golden)
130
+ kills, survivors = 0, []
131
+ for m in chk.mutations:
132
+ if chk.predicate(mutate(golden, m)) == DEVIATION: kills += 1
133
+ else: survivors.append(m)
134
+ kill_safe = (clean == CLEAN and not survivors)
135
+ status = clean if kill_safe else (clean if clean == DEVIATION else INCONCLUSIVE)
136
+ res = [CheckResult(rid, chk.keyword, status, kill_safe) for rid in chk.req_ids]
137
+ return res, {"clean": clean, "kills": f"{kills}/{len(chk.mutations)}",
138
+ "kill_safe": kill_safe, "survivors": survivors}
139
+
140
+ # ---- inscope MUSTs from the register (coverage denominator) -----------------
141
+ def inscope_musts(version, transports=("rest", "any")):
142
+ ids = set()
143
+ for f in glob.glob(str(REQ_DIR / version / "*.json")):
144
+ for r in json.load(open(f)).get("rows", []):
145
+ if r["keyword"] in ("MUST", "MUST NOT") and r["testability"] == "testable" \
146
+ and any(t in transports for t in r.get("transport", [])):
147
+ ids.add(r["id"])
148
+ return ids
149
+
150
+ def run_report(checks, base, version, scope_stamp, disclaimer, transports=("rest", "any")):
151
+ all_results, details = [], []
152
+ for chk in checks:
153
+ res, det = run_check(chk, base)
154
+ all_results += res
155
+ details.append((chk, det))
156
+ rep = aggregate(all_results, inscope_musts(version, transports), scope_stamp, disclaimer)
157
+ return rep, details