{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://wal.sh/static/schema/sponsored-onboarding-v1.json",
  "title": "sponsored-onboarding-v1",
  "description": "Onboarding manifest for sponsored-research creative sets. Lists image + text prompts, the units they bind to, the sponsored-contract-v2 field policy, and the pipeline gate commands. The manifest is authored by an operator (or the onboarding-oracle), then consumed by the assets pipeline (planner -> runner -> render).",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "$schema",
    "manifestVersion",
    "generatedAt",
    "corpus",
    "contract",
    "prompts",
    "units",
    "gates"
  ],
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri",
      "description": "Absolute URI to this JSON Schema. Must resolve."
    },
    "manifestVersion": {
      "type": "string",
      "enum": ["v1"],
      "description": "Manifest shape version. Bump when field policy changes."
    },
    "generatedAt": {
      "type": "string",
      "format": "date-time",
      "description": "ISO-8601 timestamp the manifest was authored."
    },
    "corpus": {
      "type": "object",
      "additionalProperties": false,
      "required": ["index", "docCount", "eligible", "taxonomyFrom"],
      "properties": {
        "index": { "type": "string", "description": "Path to the search index the units were drawn from." },
        "docCount": { "type": "integer", "minimum": 0, "description": "Total document count in the corpus at the time of authoring." },
        "eligible": { "type": "integer", "minimum": 0, "description": "Documents eligible for unit binding (a subset of docCount)." },
        "taxonomyFrom": { "type": "string", "description": "Rule for deriving a doc's taxonomy from index fields." },
        "note": { "type": "string", "description": "Free-form authoring note." }
      }
    },
    "contract": {
      "type": "object",
      "additionalProperties": false,
      "required": ["spec", "unitKeys", "generativeFields", "nonGenerativeFields"],
      "properties": {
        "spec": { "type": "string", "description": "Name of the contract spec the units MUST conform to." },
        "unitKeys": {
          "type": "array",
          "items": { "type": "string" },
          "uniqueItems": true,
          "description": "The keys that MUST appear on a rendered unit."
        },
        "generativeFields": {
          "type": "array",
          "items": { "type": "string" },
          "uniqueItems": true,
          "description": "Unit fields whose value is produced by a model (subject to acceptance predicates)."
        },
        "nonGenerativeFields": {
          "type": "array",
          "items": { "type": "string" },
          "uniqueItems": true,
          "description": "Unit fields whose value is read verbatim from the corpus / literals; never model-generated."
        },
        "note": { "type": "string" }
      }
    },
    "prompts": {
      "type": "object",
      "description": "Map of promptRef -> Prompt. The key is a stable id of the form prompt:<slot>/<creativeId>@<version> for images, or prompt:tagline/<class>@<version> for text.",
      "propertyNames": {
        "type": "string",
        "pattern": "^prompt:(plate|tagline)/[a-z0-9-]+(?:/[a-z0-9-]+)?@v[0-9]+$"
      },
      "additionalProperties": {
        "oneOf": [
          { "$ref": "#/$defs/imagePrompt" },
          { "$ref": "#/$defs/textPrompt" }
        ]
      }
    },
    "units": {
      "type": "array",
      "description": "The units to render. Each binds to exactly one creative + version.",
      "items": { "$ref": "#/$defs/unit" }
    },
    "gates": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Ordered list of shell commands that must exit 0 for the manifest to be considered fully onboarded."
    }
  },
  "$defs": {
    "acceptance": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Human/machine acceptance predicates the generated asset must satisfy."
    },
    "imagePrompt": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "kind", "version", "taxonomy", "scope", "target",
        "model", "params", "endpoint", "template",
        "subject", "style", "negative", "text", "acceptance"
      ],
      "properties": {
        "kind": { "const": "image" },
        "version": { "type": "string", "pattern": "^v[0-9]+$" },
        "taxonomy": { "type": "string", "description": "Corpus taxonomy this creative targets (research/tools/current/events/...)." },
        "scope": { "type": "string", "enum": ["taxonomy", "document", "global"] },
        "target": { "type": "string", "description": "Human name for what the creative renders against." },
        "model": { "type": "string", "description": "Model tag (e.g. ollama tag)." },
        "params": {
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "size": { "type": "string", "pattern": "^[0-9]+x[0-9]+$" },
            "response_format": { "type": "string" }
          }
        },
        "endpoint": { "type": "string" },
        "template": { "type": "string", "description": "Prompt template with {subject}/{style}/{negative} placeholders." },
        "subject": { "type": "string" },
        "style": { "type": "string" },
        "negative": { "type": "string" },
        "text": { "type": "string", "description": "Fully composed prompt (subject + style + negative concatenated per template)." },
        "acceptance": { "$ref": "#/$defs/acceptance" }
      }
    },
    "textPrompt": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "kind", "version", "scope",
        "model", "endpoint", "params", "text", "acceptance"
      ],
      "properties": {
        "kind": { "const": "text" },
        "version": { "type": "string", "pattern": "^v[0-9]+$" },
        "scope": { "type": "string", "enum": ["taxonomy", "document", "global"] },
        "model": { "type": "string" },
        "endpoint": { "type": "string" },
        "params": {
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "temperature": { "type": "number" },
            "num_predict": { "type": "integer", "minimum": 1 },
            "maxWords": { "type": "integer", "minimum": 1 },
            "stream": { "type": "boolean" }
          }
        },
        "text": { "type": "string", "description": "Prompt template with {title}/{description}/{keywords} placeholders." },
        "acceptance": { "$ref": "#/$defs/acceptance" },
        "renderStatus": { "type": "string", "description": "Free-form note on whether the tagline is currently allowed to render." }
      }
    },
    "unit": {
      "type": "object",
      "additionalProperties": false,
      "required": ["unitId", "taxonomy", "url", "creativeId", "creativeVersion", "status", "fields"],
      "properties": {
        "unitId": { "type": "string", "description": "Stable id, typically <taxonomy>/<slug>." },
        "taxonomy": { "type": "string" },
        "url": { "type": "string", "description": "Absolute path on the site the unit links to." },
        "creativeId": { "type": "string", "description": "The creative this unit binds to (matches a key under prompts as prompt:plate/<creativeId>@v...)." },
        "creativeVersion": { "type": "string", "pattern": "^v[0-9]+$" },
        "status": { "type": "string", "enum": ["placeholder", "generated", "live", "retired"] },
        "fields": { "$ref": "#/$defs/unitFields" }
      }
    },
    "unitFields": {
      "type": "object",
      "additionalProperties": false,
      "required": ["title", "description", "body", "matched", "docLen", "advertiser", "plate", "mark", "tagline"],
      "properties": {
        "title":       { "$ref": "#/$defs/indexField" },
        "description": { "$ref": "#/$defs/indexField" },
        "body":        { "$ref": "#/$defs/indexField" },
        "matched":     { "$ref": "#/$defs/auctionField" },
        "docLen":      { "$ref": "#/$defs/indexField" },
        "advertiser":  { "$ref": "#/$defs/literalField" },
        "plate":       { "$ref": "#/$defs/assetField" },
        "mark":        { "$ref": "#/$defs/derivedField" },
        "tagline":     { "$ref": "#/$defs/assetField" }
      }
    },
    "indexField": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "indexField"],
      "properties": {
        "source": { "const": "index", "description": "Value read verbatim from search-index.json." },
        "indexField": { "type": "string" },
        "prompt": { "type": ["null", "string"], "description": "Always null for non-generative fields." },
        "expected": { "type": ["string", "integer", "number"], "description": "V2 contract assertion — the value the runner MUST observe at render time." },
        "note": { "type": "string" }
      }
    },
    "auctionField": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "candidates"],
      "properties": {
        "source": { "const": "auction" },
        "prompt": { "type": ["null", "string"] },
        "candidates": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Keyword candidates the auction runs over."
        }
      }
    },
    "literalField": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "value"],
      "properties": {
        "source": { "const": "literal" },
        "value": { "type": "string" }
      }
    },
    "derivedField": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "fn"],
      "properties": {
        "source": { "const": "derived", "description": "Computed at render time from other unit fields." },
        "fn": { "type": "string", "description": "Function reference in the render layer." },
        "prompt": { "type": ["null", "string"] }
      }
    },
    "assetField": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "assetId", "promptRef", "output", "status"],
      "properties": {
        "source": { "const": "asset", "description": "Produced by the assets pipeline (planner/runner)." },
        "assetId": { "type": "string" },
        "promptRef": {
          "type": "string",
          "description": "Key into the top-level prompts map.",
          "pattern": "^prompt:(plate|tagline)/[a-z0-9-]+(?:/[a-z0-9-]+)?@v[0-9]+$"
        },
        "output": { "type": "string", "description": "Path template for the produced artefact (may contain {slot})." },
        "status": { "type": "string", "enum": ["pending", "generated", "blocked", "failed"] },
        "blockedBy": { "type": "string", "description": "Free-form reason when status = blocked." }
      }
    }
  }
}
