"""Generate deal-package.strict.schema.json from the catalogue + code lists. The shape schema (deal-package.schema.json) deliberately doesn't know the catalogue — vocabulary is the reference validator's job. This generator closes the JSON-Schema-only consumer gap (audit A2): a STRICT companion schema with every section and field enumerated, code-list enums inlined, datatypes mapped to JSON types, repeatable-instance patternProperties, and x_ extension slots. The catalogue remains the source of truth — regenerate whenever it changes: python generate_strict_schema.py # write python generate_strict_schema.py --check # CI: fail if the file is stale """ from __future__ import annotations import json import re import sys from pathlib import Path _HERE = Path(__file__).parent # Section/field ids must be lowercase-word tokens. Enforced here AND in # validate.check_artifacts (sec review F10): ids are interpolated into # patternProperties regexes below, so a stray metacharacter in a future # catalogue edit could corrupt the generated schema. re.escape() defends the # output; this asserts the input so the corruption surfaces at generate time. _ID_RE = re.compile(r"^[a-z][a-z0-9_]*$") def _load(name: str) -> dict: return json.loads((_HERE / name).read_text(encoding="utf-8")) def _value_schema(field: dict, code_lists: dict) -> dict: dt = field.get("datatype", "string") cl = field.get("code_list") if cl: codes = [c["code"] for c in (code_lists.get(cl) or {}).get("codes", [])] return {"enum": codes + [None]} if dt == "date": # anchored date pattern so JSON-Schema-only consumers reject "not-a-date" too # (re-review #21 — the reference validator caught it, the strict schema did not) return {"type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"} if dt in ("string", "text", "document_ref", "url"): return {"type": ["string", "null"]} if dt == "integer": return {"type": ["integer", "null"]} if dt in ("monetary_amount", "decimal"): return {"type": ["number", "null"]} if dt == "ratio": return {"type": ["number", "null"], "minimum": 0, "maximum": 1} if dt == "boolean": return {"type": ["boolean", "null"]} if dt == "list_string": return {"type": ["array", "null"], "items": {"type": "string"}} return {} # unknown datatype: unconstrained (check_artifacts flags the catalogue) def generate() -> dict: catalogue = _load("field-catalogue.v0.json") code_lists = _load("code-lists.v0.json").get("lists") or {} shape = _load("deal-package.schema.json") # Never generate a schema from an incoherent catalogue (re-review #22): a missing # code list becomes enum:[null], an empty one accepts nothing, a bad datatype # becomes {} (accepts everything) — a "successful" but wrong schema. Fail instead. # Load the sibling reference validator by path so it works whether the generator is # run as a script (cwd on path) or imported via importlib from another directory. import importlib.util as _ilu _spec = _ilu.spec_from_file_location("validate_ref", _HERE / "validate.py") _v = _ilu.module_from_spec(_spec) _spec.loader.exec_module(_v) arte = _v.check_artifacts(catalogue) if arte: raise ValueError("refusing to generate from an inconsistent catalogue:\n " + "\n ".join(arte)) section_defs: dict[str, dict] = {} section_props: dict[str, dict] = {} pattern_props: dict[str, dict] = {} for sec in catalogue["sections"]: sid = sec["id"] if not _ID_RE.match(sid): raise ValueError(f"section id {sid!r} is not a safe [a-z][a-z0-9_]* token") props = {} for f in sec["fields"]: if not _ID_RE.match(f["id"]): raise ValueError(f"field id {f['id']!r} (section {sid!r}) is not a safe token") props[f["id"]] = { "type": "object", "required": ["value", "provenance"], "additionalProperties": False, "properties": { "value": _value_schema(f, code_lists), "provenance": {"$ref": "#/$defs/provenance"}, }, } section_defs[f"sec_{sid}"] = { "type": "object", "properties": props, "patternProperties": {"^x_[a-z][a-z0-9_]*$": {"$ref": "#/$defs/extension_field"}}, "additionalProperties": False, } section_props[sid] = {"$ref": f"#/$defs/sec_{sid}"} if sec.get("repeatable"): # re.escape(sid) so a section id can never inject regex metacharacters # into the instance patternProperties (sec review F10). pattern_props[f"^{re.escape(sid)}:([2-9]|[1-9][0-9]+)$"] = {"$ref": f"#/$defs/sec_{sid}"} strict = { "$schema": "https://json-schema.org/draft/2020-12/schema", # Derive from the shape schema's $id so the strict $id can never drift out of # version lockstep with it (sec review IR3/F10 — was pinned at a stale v0.4.0). "$id": shape["$id"].replace("deal-package.schema.json", "deal-package.strict.schema.json"), "title": "Lending Deal-Package Readiness — pack instance (STRICT, generated)", "description": ( "GENERATED from field-catalogue.v0.json " f"(v{catalogue.get('catalogue_version')}) + code-lists.v0.json — do not edit by " "hand; run generate_strict_schema.py. Vocabulary-complete JSON Schema: every " "section/field enumerated, code-list enums inlined, datatypes mapped to JSON " "types, repeatable instances (':2', ':3', … — no leading zeros) and x_ extension " "fields as patternProperties. The reference validator remains normative for the " "semantic rules JSON Schema cannot express (tag/provenance compatibility, " "readiness coherence, the entity gate, requirement profiles)." ), "type": "object", "required": shape["required"], "properties": { **{k: v for k, v in shape["properties"].items() if k != "sections"}, "sections": { "type": "object", "minProperties": 1, "properties": section_props, "patternProperties": pattern_props, "additionalProperties": False, }, }, "additionalProperties": False, "$defs": { "extension_field": { "type": "object", "required": ["value", "provenance"], "additionalProperties": False, "properties": { "value": {}, "provenance": {"$ref": "#/$defs/provenance"}, }, }, "provenance": shape["$defs"]["provenance"], "readiness": shape["$defs"]["readiness"], **section_defs, }, } return strict def main() -> int: # Reject unknown args (re-review #37): a typo like `--chek` must NOT silently rewrite # the schema instead of checking it. unknown = [a for a in sys.argv[1:] if a != "--check"] if unknown: print(f"unknown argument(s): {unknown} — usage: generate_strict_schema.py [--check]") return 2 out = _HERE / "deal-package.strict.schema.json" text = json.dumps(generate(), indent=2, ensure_ascii=False) + "\n" if "--check" in sys.argv: if not out.exists() or out.read_text(encoding="utf-8") != text: print("STALE: deal-package.strict.schema.json does not match the catalogue — " "run: python generate_strict_schema.py") return 1 print("strict schema in sync with the catalogue") return 0 out.write_text(text, encoding="utf-8", newline="\n") print(f"wrote {out.name} ({len(text)//1024} KB)") return 0 if __name__ == "__main__": raise SystemExit(main())