From 648e20fc0c7cf4fcfb1723572e1f2f729d02f677 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:13:16 -0400 Subject: [PATCH 1/2] spec: referential-integrity gate for contracts/ + schemas/ (P2.11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes the sourceos-spec ref-gate (SourceOS-Linux/sourceos-spec #277) to prophet-platform: ~288 schemas across contracts/ + schemas/ with cross-file $refs, and nothing checked they resolve. A $ref to a moved/renamed target is a paper contract. Adapted for a MIXED tree (unlike sourceos-spec's single-purpose schemas/): only files declaring $schema/$id are treated as schemas; data/examples are skipped, not metaschema-checked. $refs resolve path-relative (referring dir first), then by basename, then by $id. Self-excluding; inline negative control proves teeth every run; fails closed on an empty scan. 5 tests. Wired into make validate. First run: 288 schemas valid, all 215 $refs resolve — clean today, kept clean by a gate that can fail. NOTE: main's Makefile currently has duplicate .PHONY/validate: lines (a merge artifact from #1346 + #1351 both appending). Functional (make unions prereqs) but worth a dedupe follow-up; not fixed here to avoid conflicting with in-flight PRs. --- Makefile | 11 +- .../tests/test_validate_schema_references.py | 49 +++++ tools/validate_schema_references.py | 172 ++++++++++++++++++ 3 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 tools/tests/test_validate_schema_references.py create mode 100644 tools/validate_schema_references.py diff --git a/Makefile b/Makefile index 086f7203..a4d707a0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -.PHONY: engine-guards validate validate-repo validate-regis-acr-integration smoke-regis-acr-service validate-provable-ai-ops-exchange docs-check drift-check standards-check topology-check chronos-evidence-loop-readout-validate lattice-surfaces-check lattice-surface-ingestor-smoke lattice-studio-smoke grlplus-service-smoke grl-mesh-smoke sophos-reasoner-smoke entity-resolution-smoke validate-ops-fabric validate-search-academy-deploy validate-search-image-release validate-lampstand-lifecycle validate-zone-stack-audit policy-fabric-endpoint-client-smoke policy-fabric-guarded-workflow-smoke zone-router-publication-local-publish-smoke zone-router-publication-failure-evidence-smoke zone-router-publication-retry-state-smoke zone-router-publication-remote-broker-seam-smoke zone-router-publication-dead-letter-smoke validate-workroom-update-contract validate-professional-intelligence-manifest validate-wallguard-professional-workroom validate-wallguard-professional-workroom-runtime validate-svf-agent-contract validate-live-sociosphere-svf-contract validate-fogstack-svf-signadot-adapter-readiness validate-environment-validate-change-v2 validate-trust-chain-contracts validate-channel-runtime-gates test-go test-python-apps test-tools smoke smoke-health smoke-eval-fabric smoke-evidence-receipts smoke-evidence-console validate-phase3 lampstand-smoke validate-phase4 lampstand-vertical-slice-smoke lampstand-zone-smoke zone-router-publication-smoke zone-router-publication-enqueue-smoke semantic-bridge-zone-validation-smoke validate-fogstack validate-storage-suite trustops-art-runner-smoke validate-workspace-services test-workspace smoke-workspace workspace-up workspace-down workspace-logs workspace-build prometheus-local-demo validate-workroom-scope-d-adversarial devsecops-workroom-demo validate-proof-artifacts validate-evidence-cover-graph validate-adr-035-contracts validate-helper-causal-receipts validate-svc-substrate-source-control validate-systema-bridge validate-workroom-schemas validate-device-orchestration validate-mutation-evidence validate-fogstack-svf-p2-evidence-gates validate-repo-governance-contracts validate-semantic-governance validate-orggov-runtime-demo validate-cronjob-script-mirrors -validate: validate-repo validate-regis-acr-integration validate-workspace-prophet-membrane-e2e validate-provable-ai-ops-exchange drift-check standards-check topology-check chronos-evidence-loop-readout-validate lattice-surfaces-check lattice-surface-ingestor-smoke lattice-studio-smoke grlplus-service-smoke grl-mesh-smoke sophos-reasoner-smoke entity-resolution-smoke validate-ops-fabric validate-search-academy-deploy validate-search-image-release validate-lampstand-lifecycle validate-zone-stack-audit policy-fabric-endpoint-client-smoke policy-fabric-guarded-workflow-smoke zone-router-publication-local-publish-smoke zone-router-publication-failure-evidence-smoke zone-router-publication-retry-state-smoke zone-router-publication-remote-broker-seam-smoke zone-router-publication-dead-letter-smoke validate-workroom-update-contract validate-professional-intelligence-manifest validate-wallguard-professional-workroom validate-wallguard-professional-workroom-runtime validate-svf-agent-contract validate-live-sociosphere-svf-contract validate-fogstack-svf-signadot-adapter-readiness validate-environment-validate-change-v2 validate-trust-chain-contracts validate-channel-runtime-gates test-go validate-phase4 test-python-apps test-tools validate-fogstack validate-storage-suite trustops-art-runner-smoke validate-workroom-scope-d-adversarial devsecops-workroom-demo validate-proof-artifacts validate-evidence-cover-graph validate-adr-035-contracts validate-helper-causal-receipts validate-svc-substrate-source-control validate-systema-bridge validate-workroom-schemas validate-device-orchestration validate-mutation-evidence validate-semantic-governance validate-orggov-runtime-demo validate-cronjob-script-mirrors +.PHONY: engine-guards validate validate-repo validate-regis-acr-integration smoke-regis-acr-service validate-provable-ai-ops-exchange docs-check drift-check standards-check topology-check chronos-evidence-loop-readout-validate lattice-surfaces-check lattice-surface-ingestor-smoke lattice-studio-smoke grlplus-service-smoke grl-mesh-smoke sophos-reasoner-smoke entity-resolution-smoke validate-ops-fabric validate-search-academy-deploy validate-search-image-release validate-lampstand-lifecycle validate-zone-stack-audit policy-fabric-endpoint-client-smoke policy-fabric-guarded-workflow-smoke zone-router-publication-local-publish-smoke zone-router-publication-failure-evidence-smoke zone-router-publication-retry-state-smoke zone-router-publication-remote-broker-seam-smoke zone-router-publication-dead-letter-smoke validate-workroom-update-contract validate-professional-intelligence-manifest validate-wallguard-professional-workroom validate-wallguard-professional-workroom-runtime validate-svf-agent-contract validate-live-sociosphere-svf-contract validate-fogstack-svf-signadot-adapter-readiness validate-environment-validate-change-v2 validate-trust-chain-contracts validate-channel-runtime-gates test-go test-python-apps test-tools smoke smoke-health smoke-eval-fabric smoke-evidence-receipts smoke-evidence-console validate-phase3 lampstand-smoke validate-phase4 lampstand-vertical-slice-smoke lampstand-zone-smoke zone-router-publication-smoke zone-router-publication-enqueue-smoke semantic-bridge-zone-validation-smoke validate-fogstack validate-storage-suite trustops-art-runner-smoke validate-workspace-services test-workspace smoke-workspace workspace-up workspace-down workspace-logs workspace-build prometheus-local-demo validate-workroom-scope-d-adversarial devsecops-workroom-demo validate-proof-artifacts validate-evidence-cover-graph validate-adr-035-contracts validate-helper-causal-receipts validate-svc-substrate-source-control validate-systema-bridge validate-workroom-schemas validate-device-orchestration validate-mutation-evidence validate-fogstack-svf-p2-evidence-gates validate-repo-governance-contracts validate-semantic-governance validate-orggov-runtime-demo validate-cronjob-script-mirrors validate-schema-references +validate: validate-repo validate-regis-acr-integration validate-workspace-prophet-membrane-e2e validate-provable-ai-ops-exchange drift-check standards-check topology-check chronos-evidence-loop-readout-validate lattice-surfaces-check lattice-surface-ingestor-smoke lattice-studio-smoke grlplus-service-smoke grl-mesh-smoke sophos-reasoner-smoke entity-resolution-smoke validate-ops-fabric validate-search-academy-deploy validate-search-image-release validate-lampstand-lifecycle validate-zone-stack-audit policy-fabric-endpoint-client-smoke policy-fabric-guarded-workflow-smoke zone-router-publication-local-publish-smoke zone-router-publication-failure-evidence-smoke zone-router-publication-retry-state-smoke zone-router-publication-remote-broker-seam-smoke zone-router-publication-dead-letter-smoke validate-workroom-update-contract validate-professional-intelligence-manifest validate-wallguard-professional-workroom validate-wallguard-professional-workroom-runtime validate-svf-agent-contract validate-live-sociosphere-svf-contract validate-fogstack-svf-signadot-adapter-readiness validate-environment-validate-change-v2 validate-trust-chain-contracts validate-channel-runtime-gates test-go validate-phase4 test-python-apps test-tools validate-fogstack validate-storage-suite trustops-art-runner-smoke validate-workroom-scope-d-adversarial devsecops-workroom-demo validate-proof-artifacts validate-evidence-cover-graph validate-adr-035-contracts validate-helper-causal-receipts validate-svc-substrate-source-control validate-systema-bridge validate-workroom-schemas validate-device-orchestration validate-mutation-evidence validate-semantic-governance validate-orggov-runtime-demo validate-cronjob-script-mirrors validate-schema-references .PHONY: engine-guards validate validate-repo validate-regis-acr-integration smoke-regis-acr-service validate-provable-ai-ops-exchange docs-check drift-check standards-check validate-argocd-source-sovereignty topology-check chronos-evidence-loop-readout-validate lattice-surfaces-check lattice-surface-ingestor-smoke lattice-studio-smoke grlplus-service-smoke grl-mesh-smoke sophos-reasoner-smoke entity-resolution-smoke validate-ops-fabric validate-search-academy-deploy validate-search-image-release validate-lampstand-lifecycle validate-zone-stack-audit policy-fabric-endpoint-client-smoke policy-fabric-guarded-workflow-smoke zone-router-publication-local-publish-smoke zone-router-publication-failure-evidence-smoke zone-router-publication-retry-state-smoke zone-router-publication-remote-broker-seam-smoke zone-router-publication-dead-letter-smoke validate-workroom-update-contract validate-professional-intelligence-manifest validate-wallguard-professional-workroom validate-wallguard-professional-workroom-runtime validate-svf-agent-contract validate-live-sociosphere-svf-contract validate-fogstack-svf-signadot-adapter-readiness validate-environment-validate-change-v2 validate-trust-chain-contracts validate-channel-runtime-gates test-go test-python-apps test-tools smoke smoke-health smoke-eval-fabric smoke-evidence-receipts smoke-evidence-console validate-phase3 lampstand-smoke validate-phase4 lampstand-vertical-slice-smoke lampstand-zone-smoke zone-router-publication-smoke zone-router-publication-enqueue-smoke semantic-bridge-zone-validation-smoke validate-fogstack validate-storage-suite trustops-art-runner-smoke validate-workspace-services test-workspace smoke-workspace workspace-up workspace-down workspace-logs workspace-build prometheus-local-demo validate-workroom-scope-d-adversarial devsecops-workroom-demo validate-proof-artifacts validate-evidence-cover-graph validate-adr-035-contracts validate-helper-causal-receipts validate-svc-substrate-source-control validate-systema-bridge validate-workroom-schemas validate-device-orchestration validate-mutation-evidence validate-fogstack-svf-p2-evidence-gates validate-repo-governance-contracts validate-semantic-governance validate-orggov-runtime-demo validate: validate-repo validate-regis-acr-integration validate-workspace-prophet-membrane-e2e validate-provable-ai-ops-exchange drift-check standards-check validate-argocd-source-sovereignty topology-check chronos-evidence-loop-readout-validate lattice-surfaces-check lattice-surface-ingestor-smoke lattice-studio-smoke grlplus-service-smoke grl-mesh-smoke sophos-reasoner-smoke entity-resolution-smoke validate-ops-fabric validate-search-academy-deploy validate-search-image-release validate-lampstand-lifecycle validate-zone-stack-audit policy-fabric-endpoint-client-smoke policy-fabric-guarded-workflow-smoke zone-router-publication-local-publish-smoke zone-router-publication-failure-evidence-smoke zone-router-publication-retry-state-smoke zone-router-publication-remote-broker-seam-smoke zone-router-publication-dead-letter-smoke validate-workroom-update-contract validate-professional-intelligence-manifest validate-wallguard-professional-workroom validate-wallguard-professional-workroom-runtime validate-svf-agent-contract validate-live-sociosphere-svf-contract validate-fogstack-svf-signadot-adapter-readiness validate-environment-validate-change-v2 validate-trust-chain-contracts validate-channel-runtime-gates test-go validate-phase4 test-python-apps test-tools validate-fogstack validate-storage-suite trustops-art-runner-smoke validate-workroom-scope-d-adversarial devsecops-workroom-demo validate-proof-artifacts validate-evidence-cover-graph validate-adr-035-contracts validate-helper-causal-receipts validate-svc-substrate-source-control validate-systema-bridge validate-workroom-schemas validate-device-orchestration validate-mutation-evidence validate-semantic-governance validate-orggov-runtime-demo @@ -42,6 +42,13 @@ controls-census: # gate fails the build if any such copy drifts from its source — two scripts, one truth. validate-cronjob-script-mirrors: python3 tools/verify_cronjob_script_mirrors.py + +# Referential-integrity gate for contracts/ + schemas/: every JSON Schema is valid and every +# $ref resolves. Generalizes the sourceos-spec ref-gate (#277) to this repo's ~288 schemas; only +# files declaring $schema/$id are checked (data/examples are skipped). Self-testing; make validate. +validate-schema-references: + python3 -m pip install --user jsonschema >/dev/null 2>&1 || true + python3 tools/validate_schema_references.py # Sovereignty gate: every ArgoCD Application must pull from our own git or the sovereign # registry, not a public Helm CDN. Shrink-only ratchet — a NEW external source fails; the ~6 # charts still on public CDNs are allowlisted and must be migrated to zot then deleted. #1343. diff --git a/tools/tests/test_validate_schema_references.py b/tools/tests/test_validate_schema_references.py new file mode 100644 index 00000000..1bfcfa4c --- /dev/null +++ b/tools/tests/test_validate_schema_references.py @@ -0,0 +1,49 @@ +"""The ref-gate must DISCRIMINATE: a dangling $ref fails, a resolving one passes, and data files +are not mistaken for schemas. Tested on synthetic inputs so it never depends on the live tree.""" +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "tools")) +import validate_schema_references as v # noqa: E402 + + +def test_is_schema_discriminates_data_from_schema(): + assert v._is_schema({"$id": "x", "type": "object"}) is True + assert v._is_schema({"$schema": "..."}) is True + assert v._is_schema({"foo": "bar"}) is False # an example / data file + assert v._is_schema(["not", "an", "object"]) is False + + +def test_resolve_pointer_walks_defs_and_arrays(): + doc = {"$defs": {"a": {"type": "string"}}, "allOf": [{"x": 1}]} + assert v.resolve_pointer(doc, "/$defs/a") is True + assert v.resolve_pointer(doc, "/allOf/0") is True + assert v.resolve_pointer(doc, "/$defs/missing") is False + assert v.resolve_pointer(doc, "/allOf/9") is False + + +def test_resolve_ref_by_id_name_and_dangling(): + good = {"$id": "https://x/Good.json", "$defs": {"a": {}}} + p = Path("/tmp/x.json").resolve() + kw = dict(by_path={p: good}, by_name={"Good.json": good}, by_id={good["$id"]: good}) + assert v.resolve_ref("Good.json", from_path=p, **kw)[0] is True + assert v.resolve_ref(good["$id"], from_path=p, **kw)[0] is True + assert v.resolve_ref("Nope.json", from_path=p, **kw)[0] is False + + +def test_end_to_end_catches_a_dangling_ref(tmp_path, monkeypatch): + (tmp_path / "contracts").mkdir() + (tmp_path / "contracts" / "Good.json").write_text('{"$id":"g","$defs":{"a":{"type":"string"}}}') + (tmp_path / "contracts" / "Bad.json").write_text('{"$id":"b","properties":{"x":{"$ref":"#/$defs/nope"}}}') + (tmp_path / "contracts" / "example.json").write_text('{"just":"data"}') # skipped, not a schema + monkeypatch.setattr(v, "ROOT", tmp_path) + findings, n, refs = v.check_all(tmp_path) + assert n == 2 # only the two schemas, not the data file + assert any("Bad.json" in name for name, _ in findings) + + +def test_self_test_exit_code_is_zero(capsys): + assert v.main(["--self-test"]) == 0 diff --git a/tools/validate_schema_references.py b/tools/validate_schema_references.py new file mode 100644 index 00000000..8222c219 --- /dev/null +++ b/tools/validate_schema_references.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Referential-integrity gate for prophet-platform's JSON Schemas. + +The same declared-not-enforced defect the sourceos-spec ref-gate (SourceOS-Linux/sourceos-spec +#277) closes exists here: contracts/ + schemas/ hold ~288 real schemas with cross-file $refs, and +nothing checks that a $ref to a moved/renamed/typo'd target still resolves. A schema promising a +contract ("this field conforms to Other.json#/$defs/x") that points at a hole is a paper contract. + +This is the sourceos-spec gate generalized to a MIXED tree: contracts/ and schemas/ also hold +data/examples, so — unlike the single-purpose sourceos-spec schemas/ dir — this only treats a file +as a schema when it declares `$schema` or `$id`; data/example JSON is skipped, not metaschema- +checked. $refs resolve path-relative (referring file's dir first), then by basename, then by $id. + +For every schema it asserts: valid JSON Schema (Draft 2020-12 metaschema) + every $ref resolves to +a real schema AND a real pointer target. Self-excluding (lives in tools/, never scanned). Teeth +proven every run by an inline synthetic negative control that never touches the real tree. + + validate_schema_references.py # scan contracts/ + schemas/ +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +try: + from jsonschema import Draft202012Validator + from jsonschema.exceptions import SchemaError +except ImportError: # pragma: no cover + sys.stderr.write("needs jsonschema (pip install --user jsonschema)\n") + raise + +ROOT = Path(__file__).resolve().parents[1] +SCAN_DIRS = ["contracts", "schemas"] + + +def _is_schema(doc) -> bool: + """A JSON file is a schema (vs data/example) if it declares $schema or $id.""" + return isinstance(doc, dict) and ("$schema" in doc or "$id" in doc) + + +def iter_refs(node): + if isinstance(node, dict): + for k, v in node.items(): + if k == "$ref" and isinstance(v, str): + yield v + else: + yield from iter_refs(v) + elif isinstance(node, list): + for it in node: + yield from iter_refs(it) + + +def resolve_pointer(doc, fragment: str) -> bool: + parts = [p.replace("~1", "/").replace("~0", "~") + for p in fragment.lstrip("/").split("/") if p != ""] + node = doc + for part in parts: + if isinstance(node, dict): + if part not in node: + return False + node = node[part] + elif isinstance(node, list): + try: + idx = int(part) + except ValueError: + return False + if not (0 <= idx < len(node)): + return False + node = node[idx] + else: + return False + return True + + +def resolve_ref(ref: str, *, from_path: Path, by_path, by_name, by_id) -> tuple[bool, str]: + file_part, _, fragment = ref.partition("#") + if file_part == "": + target = by_path.get(from_path.resolve()) + else: + # path-relative to the referring file first (most correct in a multi-dir tree) + cand = (from_path.parent / file_part).resolve() + target = by_path.get(cand) or by_name.get(Path(file_part).name) or by_id.get(file_part) + if target is None: + return False, f"$ref to missing schema: {ref!r}" + if fragment.strip("/") == "": + return True, "" + return (True, "") if resolve_pointer(target, fragment) else (False, f"$ref pointer unresolved: {ref!r}") + + +def load_all(root: Path): + by_path, by_name, by_id = {}, {}, {} + for rel in SCAN_DIRS: + for path in sorted((root / rel).rglob("*.json")) if (root / rel).is_dir() else []: + try: + doc = json.loads(path.read_text()) + except (json.JSONDecodeError, OSError): + continue + if not _is_schema(doc): + continue + rp = path.resolve() + by_path[rp] = doc + by_name.setdefault(path.name, doc) # first wins on basename collision + if isinstance(doc.get("$id"), str): + by_id[doc["$id"]] = doc + return by_path, by_name, by_id + + +def check_all(root: Path): + by_path, by_name, by_id = load_all(root) + findings, ref_count = [], 0 + for rp, doc in by_path.items(): + name = str(rp.relative_to(root)) if rp.is_relative_to(root) else rp.name + try: + Draft202012Validator.check_schema(doc) + except SchemaError as e: + findings.append((name, f"not a valid JSON Schema: {e.message}")) + for ref in iter_refs(doc): + ref_count += 1 + ok, why = resolve_ref(ref, from_path=rp, by_path=by_path, by_name=by_name, by_id=by_id) + if not ok: + findings.append((name, why)) + return findings, len(by_path), ref_count + + +def _negative_control() -> bool: + good = {"$id": "https://x/_NC.json", "type": "object", "$defs": {"a": {"type": "string"}}} + p = Path("/tmp/_nc.json").resolve() # resolve so keys match resolve_ref's from_path.resolve() + by_path, by_name, by_id = {p: good}, {"_NC.json": good}, {good["$id"]: good} + checks = [ + ("dangling internal pointer caught", + resolve_ref("#/$defs/missing", from_path=p, by_path=by_path, by_name=by_name, by_id=by_id)[0] is False), + ("dangling file ref caught", + resolve_ref("Nope.json", from_path=p, by_path=by_path, by_name=by_name, by_id=by_id)[0] is False), + ("valid internal pointer resolves", + resolve_ref("#/$defs/a", from_path=p, by_path=by_path, by_name=by_name, by_id=by_id)[0] is True), + ("valid $id ref resolves", + resolve_ref(good["$id"], from_path=p, by_path=by_path, by_name=by_name, by_id=by_id)[0] is True), + ("data file is not a schema", not _is_schema({"foo": "bar"})), + ("schema is a schema", _is_schema(good)), + ] + ok = all(v for _, v in checks) + for n, v in checks: + print(f" {'OK ' if v else 'FAIL'} negative control: {n}") + return ok + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(description="Referential-integrity gate for contracts/ + schemas/.") + ap.add_argument("--self-test", action="store_true") + args = ap.parse_args(argv) + if not _negative_control(): + print("FAIL: negative control did not trip — no teeth; refusing to certify") + return 2 + if args.self_test: + return 0 + findings, n, refs = check_all(ROOT) + if n == 0: + print("FAIL: no schemas found — refusing to green a scan of nothing") + return 2 + if findings: + print(f"FAIL: {len(findings)} referential-integrity defect(s) across {n} schemas:") + for name, why in sorted(findings): + print(f" {name}: {why}") + return 1 + print(f"OK: {n} schemas valid; all {refs} $refs resolve to a real target") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 56fbfb30d0ad0e3a49a04a0f2055c02f77c44753 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:38:28 -0400 Subject: [PATCH 2/2] harden: allowlist the standard external meta-schema $ref (json-schema.org) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial hardening: a legitimate $ref to https://json-schema.org/... was false-flagged as a dangling reference (not in the local registry). Now standard external meta-schemas resolve; any OTHER external URL still fails (vendor it or it's a typo — the gate stays strict). +1 test (json-schema.org passes, unknown external fails). --- tools/tests/test_validate_schema_references.py | 9 +++++++++ tools/validate_schema_references.py | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/tools/tests/test_validate_schema_references.py b/tools/tests/test_validate_schema_references.py index 1bfcfa4c..d9fedeaa 100644 --- a/tools/tests/test_validate_schema_references.py +++ b/tools/tests/test_validate_schema_references.py @@ -34,6 +34,15 @@ def test_resolve_ref_by_id_name_and_dangling(): assert v.resolve_ref("Nope.json", from_path=p, **kw)[0] is False +def test_external_metaschema_ref_is_not_a_false_positive(): + # adversarial: a legit $ref to the standard meta-schema must NOT be flagged as dangling… + p = Path("/tmp/x.json").resolve() + kw = dict(by_path={}, by_name={}, by_id={}) + assert v.resolve_ref("https://json-schema.org/draft/2020-12/schema", from_path=p, **kw)[0] is True + # …but an UNKNOWN external URL still fails (vendor it, or it's a typo — the gate stays strict) + assert v.resolve_ref("https://evil.example.com/schema.json", from_path=p, **kw)[0] is False + + def test_end_to_end_catches_a_dangling_ref(tmp_path, monkeypatch): (tmp_path / "contracts").mkdir() (tmp_path / "contracts" / "Good.json").write_text('{"$id":"g","$defs":{"a":{"type":"string"}}}') diff --git a/tools/validate_schema_references.py b/tools/validate_schema_references.py index 8222c219..15325441 100644 --- a/tools/validate_schema_references.py +++ b/tools/validate_schema_references.py @@ -74,10 +74,18 @@ def resolve_pointer(doc, fragment: str) -> bool: return True +# Standard external meta-schemas: a $ref to these is legitimate and out of scope for LOCAL +# referential integrity (we neither can nor need to resolve them here). Any OTHER external URL +# is still flagged — it should be vendored, or it is a typo. +EXTERNAL_OK_HOSTS = ("json-schema.org",) + + def resolve_ref(ref: str, *, from_path: Path, by_path, by_name, by_id) -> tuple[bool, str]: file_part, _, fragment = ref.partition("#") if file_part == "": target = by_path.get(from_path.resolve()) + elif file_part.startswith(("http://", "https://")) and any(h in file_part for h in EXTERNAL_OK_HOSTS): + return True, "" # standard external meta-schema — legitimately unresolvable locally else: # path-relative to the referring file first (most correct in a multi-dir tree) cand = (from_path.parent / file_part).resolve()