{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://askvouch.com/schemas/deal-package-readiness/v0.5.0/deal-package.schema.json",
  "title": "Lending Deal-Package Readiness — pack instance",
  "description": "Validates a FILLED deal package: every fact carries a value + provenance, and the pack carries a readiness verdict. Provenance and readiness are first-class — a publishable layer no open lending standard currently provides. WHICH fields may appear is governed by field-catalogue (see catalogue_version). NOTE: the $id namespace is provisional (askvouch.com, owned) pending a neutral home and name clearance.",
  "type": "object",
  "required": [
    "standard",
    "schema_version",
    "catalogue_version",
    "entity",
    "sections",
    "readiness"
  ],
  "properties": {
    "standard": {
      "const": "lending-deal-package-readiness"
    },
    "schema_version": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+$"
    },
    "catalogue_version": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+$"
    },
    "entity": {
      "type": "object",
      "required": [
        "legal_form"
      ],
      "additionalProperties": false,
      "properties": {
        "legal_form": {
          "description": "Broad enum so a pack can DESCRIBE any applicant. The eligibility GATE is a readiness rule (enforced by the reference validator): oven-ready / L2+ / attested packs require a corporate form (ltd/llp/plc). sole_trader and small-partnership lending can be a regulated credit agreement (RAO art 60B, subject to the art 60C exemptions; partnerships via art 60L) — such packs may relate to a regulated credit agreement and implementers carry the corresponding FCA obligations; a dedicated regulated-agreement profile is future work, not covered by v0.",
          "enum": [
            "ltd",
            "llp",
            "plc",
            "partnership",
            "sole_trader",
            "other"
          ]
        },
        "jurisdiction": {
          "type": "string",
          "default": "GB"
        }
      }
    },
    "sections": {
      "type": "object",
      "minProperties": 1,
      "description": "section_id -> { field_id -> field }. Legal section/field ids are governed by the field catalogue — enforced by the reference validator (cross_check_catalogue), not by this schema document. REPEATING GROUPS (v0.3, additive): sections the catalogue marks repeatable:true may appear as further instances suffixed ':2', ':3', … (e.g. 'guarantor', 'guarantor:2') — each instance with its own per-fact provenance. Instances of non-repeatable sections are rejected by the reference validator.",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": {
          "$ref": "#/$defs/field"
        }
      }
    },
    "producer": {
      "type": "object",
      "description": "OPTIONAL but RECOMMENDED (v0.5.0): who assembled this pack. In v0 provenance is the producer's recorded claim rather than a proof a consumer can check independently, so trusting a pack means trusting its producer. This block lets a consumer know whose claim it is, and what regulatory status that party holds.",
      "required": [
        "name"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "description": "The legal or trading name of the party that assembled the pack."
        },
        "role": {
          "type": "string",
          "description": "How the producer acted on this case, e.g. 'origination platform', 'broker', 'lender'."
        },
        "fca_status": {
          "description": "The producer's FCA position for the activity carried on in assembling and passing this pack. not_stated is honest; omitting the block entirely says nothing at all.",
          "enum": [
            "authorised",
            "appointed_representative",
            "unregulated",
            "not_applicable",
            "not_stated"
          ]
        },
        "fca_frn": {
          "type": "string",
          "description": "The Firm Reference Number on the FCA Register, where the producer is authorised or an appointed representative. Checkable against the fca_register entry in reference-data."
        },
        "principal_name": {
          "type": "string",
          "description": "Where fca_status is appointed_representative: the principal firm that carries regulatory responsibility (FSMA s.39)."
        },
        "contact": {
          "type": "string",
          "description": "A contact address for questions about this pack."
        }
      }
    },
    "readiness": {
      "$ref": "#/$defs/readiness"
    },
    "signatures": {
      "type": "array",
      "description": "OPTIONAL evidence anchors (v1.0 layer, see spec/evidence-anchors-design.md). Each entry is a DETACHED JWS (RFC 7515) over the RFC 8785 JCS canonical bytes named by `over`: 'pack' = the whole document minus this signatures block; 'attestation' = the attestation tuple bound to the pack's content hash, so an attestation cannot be replayed onto a different pack. The signing algorithm is read from the SIGNED protected header, never from an unprotected field, so an algorithm-substitution attack cannot downgrade a signature. Structure only is checked here; authenticity requires a public key the consumer supplies (schema/evidence.py verify_signatures) — the reference validator never fetches a key. NOT yet required by any conformance level: that is the v1.0 rule change.",
      "maxItems": 16,
      "items": {
        "type": "object",
        "required": [
          "over",
          "protected",
          "signature"
        ],
        "additionalProperties": false,
        "properties": {
          "over": {
            "description": "What the signature covers.",
            "enum": [
              "pack",
              "attestation"
            ]
          },
          "protected": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]+$",
            "description": "base64url (unpadded) JOSE protected header — carries the signed `alg` and `kid`, and optionally `frn` binding the key to the producer's FCA Firm Reference Number."
          },
          "signature": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]+$",
            "description": "base64url (unpadded) signature. ES256 is raw R||S per JOSE, not DER."
          }
        }
      }
    }
  },
  "additionalProperties": false,
  "$defs": {
    "field": {
      "type": "object",
      "required": [
        "value",
        "provenance"
      ],
      "properties": {
        "value": {
          "type": [
            "string",
            "number",
            "boolean",
            "null",
            "array",
            "object"
          ]
        },
        "provenance": {
          "$ref": "#/$defs/provenance"
        }
      },
      "additionalProperties": false
    },
    "provenance": {
      "description": "How a fact entered the pack, and when it was checked. Aligned with what a producing system (e.g. AskVouch dealpack/assemble.py) actually emits: verified facts carry the check date; document/not_collected may carry a display label.",
      "oneOf": [
        {
          "type": "object",
          "required": [
            "kind",
            "source"
          ],
          "additionalProperties": false,
          "properties": {
            "kind": {
              "const": "verified"
            },
            "source": {
              "type": "string"
            },
            "via": {
              "type": "string"
            },
            "date": {
              "type": "string"
            },
            "expires_at": {
              "type": "string"
            },
            "evidence_ref": {
              "type": "string",
              "description": "OPAQUE locator for the stored source response — a storage key, not a fetch instruction. A consumer that chooses to dereference it MUST treat it as untrusted input (allowlist schemes, block SSRF targets, bound size/type/time — see the spec's Security considerations)."
            },
            "evidence_hash": {
              "type": "string",
              "pattern": "^sha256:[0-9a-f]{64}$",
              "description": "OPTIONAL evidence anchor (v1.0 layer, spec/evidence-anchors-design.md §3b): 'sha256:' + hex of the RFC 8785 JCS bytes of the SOURCE'S OWN RESPONSE that established this fact. Lets a consumer re-hash the stored (or re-fetched) response and confirm the producer neither invented nor altered it — the difference between 'we say we checked Companies House' and 'here is the checkable trace'. NOT proof the source was honest, and NOT producer authenticity (that is the pack signature). Computed by schema/evidence.py evidence_hash(); checked by verify_evidence()."
            }
          }
        },
        {
          "type": "object",
          "required": [
            "kind"
          ],
          "additionalProperties": false,
          "properties": {
            "kind": {
              "const": "self-declared"
            }
          }
        },
        {
          "type": "object",
          "required": [
            "kind"
          ],
          "additionalProperties": false,
          "properties": {
            "kind": {
              "const": "document"
            },
            "checked": {
              "type": "boolean"
            },
            "ref": {
              "type": "string"
            },
            "label": {
              "type": "string"
            }
          }
        },
        {
          "type": "object",
          "required": [
            "kind"
          ],
          "additionalProperties": false,
          "properties": {
            "kind": {
              "const": "not_collected"
            },
            "label": {
              "type": "string"
            }
          }
        }
      ]
    },
    "readiness": {
      "type": "object",
      "required": [
        "status",
        "attested",
        "conformance_level",
        "missing"
      ],
      "additionalProperties": false,
      "properties": {
        "status": {
          "description": "oven-ready REQUIRES missing == [] (enforced by the reference validator). gaps-remaining requires a non-empty missing list.",
          "enum": [
            "oven-ready",
            "gaps-remaining"
          ]
        },
        "attested": {
          "description": "true REQUIRES declarations.truth_statement with a non-empty value AND an attestation object (enforced by the reference validator). An attestation is the packing party's statement — it does NOT transfer legal responsibility to, or discharge the diligence obligations of, a receiving firm.",
          "type": "boolean"
        },
        "attestation": {
          "type": "object",
          "required": [
            "attested_by",
            "attested_at"
          ],
          "additionalProperties": false,
          "properties": {
            "attested_by": {
              "type": "string",
              "description": "Who attests (name + capacity, e.g. 'A. Director, director of the applicant')"
            },
            "attested_at": {
              "type": "string"
            },
            "statement_ref": {
              "type": "string",
              "description": "Reference/hash of the signed truth statement"
            }
          }
        },
        "conformance_level": {
          "description": "L1 = declared-only. L2 = verify-core: no verify-tagged field present in the pack carries a self-declared value (not_collected is permitted — it is the honest absence marker), and at least one verified fact exists. L3 = L2 + attested with attestation object + oven-ready. Enforced by the reference validator.",
          "enum": [
            "L1-declared",
            "L2-verified-core",
            "L3-attested"
          ]
        },
        "missing": {
          "type": "array",
          "uniqueItems": true,
          "items": {
            "type": "string"
          }
        }
      }
    }
  }
}