{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://oods-foundry.com/schemas/dashboard.render.output.json",
  "title": "dashboard_render output",
  "description": "A composed metric-overview dashboard: the resolved abstract layout, per-panel compiled payloads (chart specs / KPI values / a11y-described error placeholders), the declared cross-filter links, the dashboard-level a11y block, and one dashboard-level specRef for pipeline reuse. Per-panel viz_render specRefs are suppressed; geo panels preserve their echartsSpec.__registration FeatureCollection.",
  "type": "object",
  "required": [
    "status",
    "panels",
    "warnings"
  ],
  "additionalProperties": false,
  "properties": {
    "status": {
      "type": "string",
      "enum": [
        "ok",
        "error"
      ],
      "description": "Whether the dashboard composed successfully (ok even when individual panels failed to a placeholder)."
    },
    "schemaVersion": {
      "type": "string",
      "description": "Echoes the DashboardSpec IR version."
    },
    "panels": {
      "type": "array",
      "description": "Per-panel compiled results, in declared panel order. A chart panel carries its compiled spec; a kpi panel carries computed values; a failed panel becomes an a11y-described error placeholder (onPanelError default).",
      "items": {
        "$ref": "#/$defs/panelResult"
      }
    },
    "layout": {
      "type": "array",
      "description": "The resolved grid placements, one per rendered panel.",
      "items": {
        "$ref": "#/$defs/placement"
      }
    },
    "links": {
      "type": "array",
      "description": "Declared panel relationships, rendered as a labelled reference list in HTML. Active filtering uses selection and crossFilter independently.",
      "items": {
        "type": "object",
        "additionalProperties": true
      }
    },
    "a11y": {
      "$ref": "#/$defs/dashboardA11y"
    },
    "tokenCssRef": {
      "type": "string",
      "description": "Deferred token CSS reference when compact mode is on (use tokens_build)."
    },
    "html": {
      "type": "string",
      "description": "Opt-in self-contained dashboard HTML with scoped chart SVG, KPI tiles and error placeholders. Light/dark renders all admitted chart types. HC chart rendering preserves declared token colors; unsupported renderer paints become typed error panels under the existing placeholder/omit policy."
    },
    "specRef": {
      "type": "string",
      "description": "One dashboard-level reference to the composed payload for pipeline reuse."
    },
    "specRefCreatedAt": {
      "type": "string"
    },
    "specRefExpiresAt": {
      "type": "string"
    },
    "contentHash": {
      "type": "string",
      "description": "Deterministic SHA-256 (hex) over the canonicalized composed payload ({panels, layout}) — the content identity of exactly what specRef caches. Unlike specRef (a random, expiring cache handle), contentHash is stable: same input yields the same hash. panels[].contentHash, panels[].normalizedSpec, and outputHtmlHash are attached after this hash is computed and sit outside the hashed projection. Default-on; omitted only on error outputs."
    },
    "outputHtmlHash": {
      "type": "string",
      "description": "SHA-256 over the exact returned HTML bytes. Successful panels contain scoped SVG; failed panels follow the declared placeholder/omit policy, including measured HC render deferrals. This per-call identity is distinct from a certified runtime-matrix renderHashEpoch claim."
    },
    "output": {
      "type": "object",
      "additionalProperties": false,
      "description": "Echoes the normalized output controls.",
      "properties": {
        "compact": {
          "type": "boolean"
        },
        "echarts": {
          "type": "boolean"
        },
        "includeNormalizedSpec": {
          "type": "boolean",
          "description": "Echoes the call-level opt-in that emits panels[].normalizedSpec for successful chart panels."
        },
        "html": {
          "type": "boolean"
        },
        "dataTable": {
          "type": "boolean"
        },
        "contrastScan": {
          "type": "boolean"
        },
        "includeA11y": {
          "type": "boolean"
        },
        "theme": {
          "type": "string",
          "enum": [
            "light",
            "dark",
            "hc"
          ],
          "description": "Echoes an explicitly requested CSS token theme."
        },
        "brand": {
          "type": "string",
          "format": "oods-brand",
          "description": "Echoes input.brand, and only when it was supplied (s169 m04) — an absent brand leaves this object byte-identical to before the field existed."
        }
      }
    },
    "meta": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "panelCount": {
          "type": "integer",
          "minimum": 0
        },
        "datasetCount": {
          "type": "integer",
          "minimum": 0
        },
        "crossFiltered": {
          "type": "boolean",
          "description": "True when an active selection was applied."
        },
        "errorPanelCount": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "errors": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/issue"
      }
    },
    "warnings": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/issue"
      }
    },
    "a11yContrast": {
      "$ref": "#/$defs/a11yContrast"
    }
  },
  "$defs": {
    "placement": {
      "type": "object",
      "required": [
        "id",
        "x",
        "y",
        "w",
        "h"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string"
        },
        "x": {
          "type": "integer",
          "minimum": 0
        },
        "y": {
          "type": "integer",
          "minimum": 0
        },
        "w": {
          "type": "integer",
          "minimum": 1
        },
        "h": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "panelResult": {
      "description": "A compiled panel result — chart, kpi, or error placeholder (discriminated on 'kind').",
      "oneOf": [
        {
          "$ref": "#/$defs/chartPanelResult"
        },
        {
          "$ref": "#/$defs/kpiPanelResult"
        },
        {
          "$ref": "#/$defs/errorPanelResult"
        }
      ]
    },
    "chartPanelResult": {
      "type": "object",
      "required": [
        "id",
        "kind",
        "chartType",
        "renderer"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string"
        },
        "kind": {
          "type": "string",
          "const": "chart"
        },
        "title": {
          "type": "string"
        },
        "chartType": {
          "type": "string"
        },
        "renderer": {
          "type": "string",
          "enum": [
            "vega-lite",
            "echarts"
          ]
        },
        "spec": {
          "type": "object",
          "additionalProperties": true,
          "description": "Compiled Vega-Lite spec (tabular), or {} for ECharts-primary panels."
        },
        "echartsSpec": {
          "type": "object",
          "additionalProperties": true,
          "description": "Compiled ECharts option (ECharts-primary panels + opt-in tabular). Geo panels carry the FeatureCollection on echartsSpec.__registration."
        },
        "contentHash": {
          "type": "string",
          "description": "The successful panel's viz_render contentHash, equal to a standalone viz_render call over the identical resolved spec and data. Attached after the dashboard hash and outside its {panels, layout} projection. This receipt is brand-invariant because brand applies later at dashboard HTML/SVG emission; it proves this-call content determinism, not certified-matrix renderHashEpoch coverage."
        },
        "normalizedSpec": {
          "type": "object",
          "additionalProperties": true,
          "description": "The panel's intermediate NormalizedVizSpec IR, present only when output.includeNormalizedSpec=true. Attached after the dashboard hash and outside its {panels, layout} projection for direct artifact_certify round-trips."
        },
        "a11yDescription": {
          "type": "string"
        },
        "a11y": {
          "$ref": "#/$defs/structuredA11y"
        }
      }
    },
    "structuredA11y": {
      "type": "object",
      "additionalProperties": false,
      "description": "Structured two-part text alternative (accessible data table + narrative summary) for this panel, derived from the same data source the panel renders from. Present only when the dashboard output.includeA11y is true (additive; default-off keeps the wire byte-identical).",
      "properties": {
        "table": {
          "type": "object",
          "required": [
            "caption",
            "columns",
            "rows"
          ],
          "additionalProperties": false,
          "properties": {
            "caption": {
              "type": "string"
            },
            "columns": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "field",
                  "label",
                  "isNumeric"
                ],
                "additionalProperties": false,
                "properties": {
                  "field": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "isNumeric": {
                    "type": "boolean"
                  }
                }
              }
            },
            "rows": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "cells"
                ],
                "additionalProperties": false,
                "properties": {
                  "cells": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "field",
                        "text"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "field": {
                          "type": "string"
                        },
                        "text": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "narrative": {
          "type": "object",
          "required": [
            "summary",
            "keyFindings"
          ],
          "additionalProperties": false,
          "properties": {
            "summary": {
              "type": "string"
            },
            "keyFindings": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "kpiPanelResult": {
      "type": "object",
      "required": [
        "id",
        "kind",
        "value",
        "trendDirection"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string"
        },
        "kind": {
          "type": "string",
          "const": "kpi"
        },
        "title": {
          "type": "string"
        },
        "value": {
          "type": "number"
        },
        "formatted": {
          "type": "string"
        },
        "delta": {
          "type": [
            "number",
            "null"
          ]
        },
        "deltaPct": {
          "type": [
            "number",
            "null"
          ]
        },
        "trendDirection": {
          "type": "string",
          "enum": [
            "increasing",
            "decreasing",
            "flat"
          ]
        },
        "sparkline": {
          "type": "array",
          "items": {
            "type": "number"
          }
        },
        "thresholdBreached": {
          "type": "boolean"
        },
        "anomaly": {
          "type": "boolean"
        },
        "a11yDescription": {
          "type": "string"
        }
      }
    },
    "errorPanelResult": {
      "type": "object",
      "required": [
        "id",
        "kind",
        "error"
      ],
      "additionalProperties": false,
      "description": "Partial-panel error policy (default 'placeholder'): a panel that failed to render, kept in-place and a11y-described, so it does not void the dashboard.",
      "properties": {
        "id": {
          "type": "string"
        },
        "kind": {
          "type": "string",
          "const": "error"
        },
        "title": {
          "type": "string"
        },
        "chartType": {
          "type": "string"
        },
        "error": {
          "$ref": "#/$defs/issue"
        },
        "a11yDescription": {
          "type": "string"
        }
      }
    },
    "dashboardA11y": {
      "type": "object",
      "required": [
        "description"
      ],
      "additionalProperties": false,
      "properties": {
        "description": {
          "type": "string"
        },
        "ariaLabel": {
          "type": "string"
        },
        "readingOrder": {
          "type": "string",
          "enum": [
            "kpi-first",
            "declared"
          ]
        },
        "panelOrder": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Panel ids in reading/focus order (KPI row first by default)."
        },
        "narrative": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "summary": {
              "type": "string"
            },
            "keyFindings": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "issue": {
      "type": "object",
      "required": [
        "code",
        "message"
      ],
      "additionalProperties": false,
      "properties": {
        "code": {
          "type": "string"
        },
        "message": {
          "type": "string"
        },
        "severity": {
          "type": "string",
          "enum": [
            "error",
            "warning"
          ]
        }
      }
    },
    "contrastFinding": {
      "type": "object",
      "required": [
        "pair",
        "ratio",
        "threshold",
        "severity"
      ],
      "additionalProperties": false,
      "properties": {
        "pair": {
          "type": "string",
          "description": "The contrast pair id (e.g. 'fg-on-bg')."
        },
        "ratio": {
          "type": "number",
          "description": "Measured WCAG contrast ratio (e.g. 3.2)."
        },
        "threshold": {
          "type": "number",
          "description": "Required minimum ratio (e.g. 4.5)."
        },
        "severity": {
          "type": "string",
          "enum": [
            "error",
            "warning"
          ],
          "description": "Always 'warning' — a failing pair, mirroring the OODS-V135 warning."
        }
      }
    },
    "a11yContrast": {
      "type": "object",
      "required": [
        "findings"
      ],
      "additionalProperties": false,
      "description": "Structured findings from the four-pair token contrast preflight — present only when output.contrastScan=true. Machine-readable mirror of the OODS-V135 warnings (the failing brand-token colour pairs from scanBrandContrast). This is not whole-dashboard WCAG certification. Absent on the default path, so the default render stays byte-identical.",
      "properties": {
        "findings": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/contrastFinding"
          }
        },
        "summary": {
          "type": "object",
          "additionalProperties": false,
          "description": "Roll-up of the contrast scan.",
          "properties": {
            "failing": {
              "type": "integer",
              "minimum": 0,
              "description": "Number of brand-token pairs below their WCAG threshold."
            },
            "gradedPairs": {
              "type": "integer",
              "minimum": 0,
              "description": "How many brand-token pairs were actually measured (s169 m04). `failing: 0` on its own cannot distinguish \"checked every pair and all passed\" from \"checked nothing\" — and until s169 it silently meant the second, because the resolved tokens are rgb() strings the ratio function rejected and the rejection was swallowed. A consumer should treat gradedPairs < the expected pair count as an incomplete scan, not a clean one."
            }
          }
        }
      }
    }
  }
}
