{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "property-preval inputs",
  "description": "Single-source contract for render.py. Mirrors the 12 property-data MCP responses for one full UK address, plus operator-supplied brand, subject (including the three EPC certificate fields pasted before invocation), and agent_listings. `subject.narrative` and `market_summary` are composed by the agent from the fan-out — not operator-supplied.",
  "type": "object",
  "required": [
    "out_dir", "filename_stem", "address", "postcode", "generated",
    "brand", "subject", "valuation", "area", "demand_trend",
    "market_summary", "agent_listings", "agent_overrides"
  ],
  "properties": {
    "out_dir":       { "type": "string", "description": "Absolute path to the output directory (created if missing)." },
    "filename_stem": { "type": "string", "description": "PDF filename stem; .pdf appended by the renderer." },
    "address":       { "type": "string", "description": "Full UK address as the operator gave it." },
    "postcode":      { "type": "string", "description": "Postcode derived from the address, e.g. 'CM23 2UJ'." },
    "generated":     { "type": "string", "description": "Human date, e.g. '19 May 2026'." },
    "variant":       { "type": "string", "enum": ["premium", "on-brand"], "description": "Render variant. 'premium' (default) keeps the editorial Cormorant/Lora/Inter typography lock and only applies the brand's colours + logo. 'on-brand' overrides the type system with brand.font_display + brand.font_body — the pack follows the agent's identity end-to-end. Decision rule: if the operator says 'premium' use premium; if brand.font_display + brand.font_body are both set and the operator did not say premium, default to 'on-brand'; otherwise premium." },

    "brand": {
      "type": "object",
      "required": ["name", "tagline", "primary", "primary_dark", "accent", "paper", "paper_banded", "rule"],
      "properties": {
        "name":         { "type": "string" },
        "tagline":      { "type": "string" },
        "primary":      { "type": "string", "description": "Hex, e.g. '#064444'." },
        "primary_dark": { "type": "string" },
        "accent":       { "type": "string" },
        "paper":        { "type": "string" },
        "paper_banded": { "type": "string" },
        "rule":         { "type": "string" },
        "logo_light":   { "type": "string", "description": "Optional filesystem path to white-on-transparent logo for the cover." },
        "logo_dark":    { "type": "string", "description": "Optional filesystem path to full-colour logo for body-page headers." },
        "agent_name":   { "type": "string", "description": "Optional. Name of the individual agent preparing the pitch — printed in the cover's 'Prepared by' meta cell. Falls back to brand.name when omitted." },
        "font_display": { "type": "string", "description": "Optional. CSS font-family value for the display typeface (h1/h2/h4/eyebrows). Only honoured when variant='on-brand'. e.g. '\"Azo Sans\", system-ui, sans-serif'." },
        "font_body":    { "type": "string", "description": "Optional. CSS font-family value for body typography (paragraphs, lists). Only honoured when variant='on-brand'. e.g. 'Poppins, sans-serif'." }
      }
    },

    "subject": {
      "type": "object",
      "required": ["cover_hero", "total_floor_area_m2", "current_energy_rating", "lodgement_date"],
      "properties": {
        "cover_hero":             { "type": "string", "description": "Filesystem path or URL to a photo of the subject property — required for the cover. Renderer asserts the file exists, is ≥ 1200×800, and is not symlinked to another property's folder." },
        "beds":                   { "type": ["integer", "null"] },
        "type":                   { "type": ["string", "null"], "description": "e.g. 'Semi-detached', 'Terraced', 'Flat'." },
        "tenure":                 { "type": ["string", "null"], "description": "e.g. 'Freehold', 'Leasehold'." },
        "condition":              { "type": ["string", "null"], "description": "'well' | 'average' | 'needs work'." },
        "total_floor_area_m2":    { "type": "number", "description": "Total internal area in square metres, copied verbatim from the EPC certificate. Operator pastes this into inputs.json before invocation. Null aborts with epc-certificate-not-supplied." },
        "current_energy_rating":  { "type": "string", "description": "Current energy rating from the EPC certificate, e.g. 'C'. Operator paste-in. Null aborts with epc-certificate-not-supplied." },
        "lodgement_date":         { "type": "string", "description": "ISO date of the EPC certificate's lodgement, e.g. '2018-04-21'. Operator paste-in. Null aborts with epc-certificate-not-supplied." },
        "narrative":              { "type": ["string", "null"], "description": "One short paragraph (1–3 sentences) under the valuation panel. Composed by the agent from EPC + council-tax records + adjustments — not operator-supplied." },
        "adjustments": {
          "type": "array",
          "description": "Operator-curated esoteric factors that shift the headline range. Deltas sum linearly into the final factor (1 + Σ delta_pct/100).",
          "items": {
            "type": "object",
            "required": ["label", "delta_pct"],
            "properties": {
              "label":     { "type": "string" },
              "delta_pct": { "type": "number", "description": "Percent change, e.g. 3 for +3%, -8 for -8%." }
            }
          }
        }
      }
    },

    "valuation": {
      "type": "object",
      "required": ["avg_asking", "asking_psf", "avg_sold", "sold_psf", "sold_psf_low", "sold_psf_high", "sold_comps", "asking_comps"],
      "properties": {
        "avg_asking":   { "type": "integer", "description": "Mean asking £ from property-data-prices." },
        "asking_psf":   { "type": "integer", "description": "Mean asking £/sqft." },
        "avg_sold":     { "type": "integer", "description": "Mean sold £ from property-data-sold-prices." },
        "sold_psf":     { "type": "integer", "description": "Mean sold £/sqft (midpoint of valuation)." },
        "sold_psf_low": { "type": "integer", "description": "Lower bound of the sold £/sqft 70pc range." },
        "sold_psf_high":{ "type": "integer", "description": "Upper bound of the sold £/sqft 70pc range." },
        "sold_range":   { "type": "array", "items": { "type": "integer" }, "minItems": 2, "maxItems": 2, "description": "Optional 70pc sold-£ range [low, high]." },
        "sold_comps": {
          "type": "array",
          "description": "Nearest sold transactions (≤ 6 rows in the table).",
          "items": {
            "type": "object",
            "required": ["date", "address", "type", "price"],
            "properties": {
              "date":    { "type": "string" },
              "address": { "type": "string" },
              "type":    { "type": "string" },
              "beds":    { "type": ["integer", "null"], "description": "Bedroom count from the Land Registry transaction; some rows are null in the source." },
              "price":   { "type": "integer" }
            }
          }
        },
        "asking_comps": {
          "type": "array",
          "description": "Live asking comps (≤ 4 tiles).",
          "items": {
            "type": "object",
            "required": ["address", "price"],
            "properties": {
              "address": { "type": "string" },
              "price":   { "type": "integer" },
              "beds":    { "type": ["integer", "null"] },
              "type":    { "type": ["string", "null"] }
            }
          }
        }
      }
    },

    "area": {
      "type": "object",
      "required": ["crime_rating", "flood_risk", "council_tax_band_d", "population", "crime_breakdown", "council_tax_bands", "headline"],
      "properties": {
        "crime_rating":       { "type": "string" },
        "flood_risk":         { "type": "string" },
        "council_tax_band_d": { "type": "integer" },
        "population":         { "type": "integer" },
        "crime_breakdown":    { "type": "array", "items": { "type": "object", "required": ["category", "count"], "properties": { "category": {"type":"string"}, "count": {"type":"integer"} } } },
        "council_tax_bands":  { "type": "array", "items": { "type": "object", "required": ["band", "amount"], "properties": { "band": {"type":"string"}, "amount": {"type":"integer"} } } },
        "headline":           { "type": "string" }
      }
    },

    "demand_trend": {
      "type": "object",
      "required": ["demand_rating", "dom", "turnover_pct", "for_sale", "growth_series", "growth_psf_series", "rental", "type_dist"],
      "properties": {
        "demand_rating":  { "type": "string" },
        "dom":            { "type": "integer" },
        "turnover_pct":   { "type": "integer" },
        "for_sale":       { "type": "integer" },
        "sales_per_month":{ "type": "integer" },
        "yoy_growth_pct": { "type": "number" },
        "growth_series": {
          "type": "array",
          "items": { "type": "object", "required": ["label", "price"], "properties": { "label": {"type":"string"}, "price": {"type":"integer"} } }
        },
        "growth_psf_series": {
          "type": "array",
          "items": { "type": "object", "required": ["label", "psf"], "properties": { "label": {"type":"string"}, "psf": {"type":"integer"} } }
        },
        "rental": {
          "type": "object",
          "required": ["demand_rating", "dom", "turnover_pct", "for_rent"],
          "properties": {
            "demand_rating": { "type": "string" },
            "dom":           { "type": "integer" },
            "turnover_pct":  { "type": "integer" },
            "for_rent":      { "type": "integer" }
          }
        },
        "type_dist": {
          "type": "array",
          "items": { "type": "object", "required": ["name", "pct"], "properties": { "name": {"type":"string"}, "pct": {"type":"number"} } }
        }
      }
    },

    "market_summary": {
      "type": "object",
      "description": "Composed by the agent from the 12-call PropertyData fan-out — not operator-supplied. See SKILL.md \"Composed by the agent from the fan-out\" for the field-by-field mapping.",
      "required": ["headline", "paragraphs", "demand_gauge_position", "price_growth_note", "demand_note"],
      "properties": {
        "headline":              { "type": "string", "description": "One-line display-italic h2 for the market context page." },
        "paragraphs":            { "type": "array", "minItems": 2, "items": { "type": "string" }, "description": "2–3 short paragraphs. First gets a drop cap. Fewer than 2 aborts the run with `market-summary-too-thin`." },
        "demand_gauge_position": { "type": "string", "description": "Percentage string for the gauge marker position, e.g. '62%'." },
        "price_growth_note":     { "type": "string" },
        "demand_note":           { "type": "string" }
      }
    },

    "agent_listings": {
      "type": "array",
      "minItems": 1,
      "maxItems": 6,
      "description": "Agent's own recent listings to feature on the closing page. Operator paste-in (not the PropertyData sample). Empty array aborts the run.",
      "items": {
        "type": "object",
        "required": ["address", "price", "status", "slug", "origin"],
        "properties": {
          "url":     { "type": ["string", "null"], "description": "Source URL — listing card becomes an <a> when present." },
          "address": { "type": "string" },
          "price":   { "type": "integer" },
          "beds":    { "type": ["integer", "null"] },
          "type":    { "type": ["string", "null"] },
          "image":   { "type": ["string", "null"], "description": "Path or URL to a tile photo for the card." },
          "status":  { "type": "string", "enum": ["Sold", "Under offer", "For sale"], "description": "Operator picks; no default. The gate aborts if status was set when the source property.json had status:null without agent_overrides.listings_status_curated." },
          "slug":    { "type": "string", "description": "Used to name the copied image file under img/." },
          "origin":  { "type": "string", "description": "'operator' when the operator typed the row by hand, or the filesystem path of the source property.json the row was copied from." }
        }
      }
    },

    "agent_overrides": {
      "type": "object",
      "description": "Explicit operator confirmations that authorise bypass of adherence gates. Each override is a named boolean that lands next to the disputed value in inputs.json — auditable post-hoc.",
      "required": ["brand_selected_by_operator", "listings_status_curated"],
      "properties": {
        "brand_selected_by_operator":   { "type": "boolean", "description": "True only when the operator told the agent which estate agent is pitching. Inferring brand from on-disk DESIGN.md files is invention." },
        "listings_status_curated":      { "type": "boolean", "description": "True only when the operator explicitly signed off on each agent_listings[].status value." }
      }
    }
  }
}
