{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://oods-foundry.com/schemas/artifact.certify.input.json",
  "title": "artifact_certify input",
  "description": "Certify an OODS Foundry NormalizedVizSpec at a CSS theme and brand (default light/A). The handler validates the permissive spec boundary against the authoritative viz-core IR schema. ECharts-primary IR is metadata-only: supply the same data operand used by viz_render for render-backed determinism, contrast and accuracy. Without data, categorical ECharts contrast reconstructs the scoped baked palette and makes no rendered-carrier measurement claim. HC contrast is exempt with reason forced-colors and no numeric grade; determinism, a11y and accuracy retain their normal evaluation. The data-backed ECharts paints come from the rendered projected option; geo render evidence remains exempt with no canvas ratio.",
  "type": "object",
  "required": [
    "spec"
  ],
  "properties": {
    "spec": {
      "type": "object",
      "description": "An OODS Foundry normalized chart spec: the normalizedSpec that viz_render returns. The handler validates it against the full spec schema.",
      "additionalProperties": true
    },
    "data": {
      "type": "object",
      "$comment": "MIRRORED COPY: the five branch subschemas below ($defs.hierarchyBranch / sankeyBranch / chordBranch / networkBranch / geoBranch, plus $defs.hierarchyNode) are copied VERBATIM from packages/mcp-server/src/schemas/viz.render.input.json's properties.{hierarchy,sankey,chord,network,geo} and $defs.hierarchyNode. A cross-schema $ref is not resolvable in this AJV setup (the same constraint this file's `spec` description records), so the copy is deliberate and DECLARED. Keep-in-step mechanism: test/tools/artifact.certify.data-operand.spec.ts asserts each branch here is deep-equal to its viz.render source, so a divergence fails the suite rather than shipping a lying contract.",
      "description": "Optional operand for the 8 ECharts-primary types (treemap/sunburst/sankey/force_graph/choropleth/bubble_map/flow_map/chord): the same data branch viz_render takes, so certify re-emits the option the render path would emit for this (spec, data) pair. Exactly one branch, and it must be the branch the IR's mark trait requires (MarkTreemap/MarkSunburst -> hierarchy, MarkSankey -> sankey, MarkChord -> chord, MarkGraph -> network, MarkChoropleth/MarkBubble/MarkFlow -> geo); a mismatch is a structured error. Supplying `data` alongside a cartesian IR (MarkBar/MarkLine/MarkPoint/MarkArea/MarkRect) or an unmodeled mark is also a structured error — those paths compile from the IR itself, so the operand would be dead input. Omitting `data` is always valid: the verdict then rates the spec alone, with determinism and accuracy honestly 'unchecked' and a note saying why.",
      "minProperties": 1,
      "maxProperties": 1,
      "properties": {
        "hierarchy": {
          "$ref": "#/$defs/hierarchyBranch"
        },
        "sankey": {
          "$ref": "#/$defs/sankeyBranch"
        },
        "chord": {
          "$ref": "#/$defs/chordBranch"
        },
        "network": {
          "$ref": "#/$defs/networkBranch"
        },
        "geo": {
          "$ref": "#/$defs/geoBranch"
        }
      },
      "additionalProperties": false
    },
    "theme": {
      "type": "string",
      "enum": [
        "light",
        "dark",
        "hc"
      ],
      "default": "light",
      "description": "CSS token theme for chart pixels, default light. HC emits the declared scope colors verbatim, including CSS system colors; their computed paints require a forced-colors browser. A renderer that substitutes undeclared paints is typed-deferred instead of returning misleading HC pixels. No server-side system-color hex palette is invented."
    },
    "brand": {
      "type": "string",
      "format": "oods-brand",
      "default": "A",
      "description": "CSS brand scope, matching viz_render for the same normalized spec and data operand."
    }
  },
  "additionalProperties": false,
  "$defs": {
    "hierarchyBranch": {
      "description": "Hierarchy data for chartType 'treemap' or 'sunburst' (explicit-only) — used instead of rows/datasetRef + x/y encodings. A discriminated union on 'type': 'adjacency_list' (flat nodes linked by parentId; a node whose parent is missing becomes its own root) or 'nested' (a single root node with children).",
      "oneOf": [
        {
          "type": "object",
          "required": [
            "type",
            "data"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "adjacency_list"
            },
            "data": {
              "type": "array",
              "minItems": 1,
              "description": "Flat node list. Each node carries id, parentId (null for roots), and a numeric value.",
              "items": {
                "type": "object",
                "required": [
                  "id",
                  "parentId",
                  "value"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "minLength": 1
                  },
                  "parentId": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "value": {
                    "type": "number"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              }
            }
          },
          "additionalProperties": false
        },
        {
          "type": "object",
          "required": [
            "type",
            "data"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "nested"
            },
            "data": {
              "$ref": "#/$defs/hierarchyNode"
            }
          },
          "additionalProperties": false
        }
      ]
    },
    "sankeyBranch": {
      "type": "object",
      "description": "Flow data for chartType 'sankey' (explicit-only) — used instead of rows/datasetRef + x/y encodings. Nodes plus value-weighted links; every link must carry a numeric 'value' (the link width IS the flow magnitude) and reference existing node names.",
      "required": [
        "nodes",
        "links"
      ],
      "properties": {
        "nodes": {
          "type": "array",
          "minItems": 1,
          "description": "Flow nodes. Each carries a unique 'name'; an optional numeric 'value' overrides the computed throughput.",
          "items": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1
              },
              "value": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        },
        "links": {
          "type": "array",
          "minItems": 1,
          "description": "Directed, value-weighted flows between nodes. Each (source, target) pair should be unique — a duplicate directed flow double-counts the link width. (Uniqueness is advisory for sankey; it is not warning-enforced.)",
          "items": {
            "type": "object",
            "required": [
              "source",
              "target",
              "value"
            ],
            "properties": {
              "source": {
                "type": "string",
                "minLength": 1
              },
              "target": {
                "type": "string",
                "minLength": 1
              },
              "value": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        }
      },
      "additionalProperties": false
    },
    "chordBranch": {
      "type": "object",
      "description": "Chord data for chartType 'chord' (explicit-only) — used instead of rows/datasetRef + x/y encodings. A native ECharts ribbon diagram: a ring of category arcs connected by weighted ribbons. Sankey-shaped — nodes plus value-weighted links; every link must carry a numeric 'value' (the ribbon width IS the flow magnitude) and reference existing node names — a link naming a node absent from 'nodes' fails loud (OODS-V147).",
      "required": [
        "nodes",
        "links"
      ],
      "properties": {
        "nodes": {
          "type": "array",
          "minItems": 1,
          "description": "Ring arcs. Each carries a unique 'name'.",
          "items": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1
              },
              "value": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        },
        "links": {
          "type": "array",
          "minItems": 1,
          "description": "Directed, value-weighted ribbons between ring arcs (matched to arcs by name). Directed: source→target and target→source are distinct ribbons (a reciprocal pair is valid data). Each (source, target) pair should be unique — a duplicate directed link double-counts the ribbon and is flagged with a warning (OODS-V148).",
          "items": {
            "type": "object",
            "required": [
              "source",
              "target",
              "value"
            ],
            "properties": {
              "source": {
                "type": "string",
                "minLength": 1
              },
              "target": {
                "type": "string",
                "minLength": 1
              },
              "value": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        }
      },
      "additionalProperties": false
    },
    "networkBranch": {
      "type": "object",
      "description": "Network data for chartType 'force_graph' (explicit-only) — used instead of rows/datasetRef + x/y encodings. Nodes (each with a unique 'id'; an optional 'group' drives category colour) and directed links (optional numeric 'value'). Every link must reference existing node ids — a link naming a node absent from 'nodes' fails loud (OODS-V147).",
      "required": [
        "nodes",
        "links"
      ],
      "properties": {
        "nodes": {
          "type": "array",
          "minItems": 1,
          "description": "Graph nodes. Each carries a unique 'id'; optional 'group' (category) and 'value' (sizing).",
          "items": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string",
                "minLength": 1
              },
              "group": {
                "type": "string"
              },
              "value": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        },
        "links": {
          "type": "array",
          "description": "Directed edges between nodes (an empty array is allowed for an all-isolated-nodes graph). Each 'source'/'target' must match a node 'id' (a dangling ref fails loud, OODS-V147). Directed: source→target and target→source are distinct edges. Each (source, target) pair should be unique — a duplicate directed edge double-counts and is flagged with a warning (OODS-V148).",
          "items": {
            "type": "object",
            "required": [
              "source",
              "target"
            ],
            "properties": {
              "source": {
                "type": "string",
                "minLength": 1
              },
              "target": {
                "type": "string",
                "minLength": 1
              },
              "value": {
                "type": "number"
              }
            },
            "additionalProperties": true
          }
        }
      },
      "additionalProperties": false
    },
    "geoBranch": {
      "type": "object",
      "description": "Geo data for chartType 'choropleth', 'bubble_map', or 'flow_map' (explicit-only) — used instead of rows/datasetRef + x/y encodings. Carries inline geometry (a GeoJSON FeatureCollection in 'geojson', or a TopoJSON Topology in 'topojson' + 'topoObjectName'), an optional 'join' that merges tabular 'rows' into features by key, and the per-type encoding: 'valueField' colours regions for choropleth; 'longitudeField'/'latitudeField' (+ optional 'sizeField'/'colorField') place points for bubble_map; 'originLongitudeField'/'originLatitudeField'/'destinationLongitudeField'/'destinationLatitudeField' (+ optional 'strengthField'/'curvature') draw origin→destination arcs for flow_map. Geometry is supplied inline — it is never fetched over the network. Note: geo specs are not self-contained (unlike treemap/sankey): the resolved FeatureCollection rides back on echartsSpec.__registration and the client re-registers the map by name before rendering. Geometry payloads can be large (a world atlas is ~100KB) — prefer a pre-simplified TopoJSON.",
      "properties": {
        "geojson": {
          "type": "object",
          "description": "Inline GeoJSON FeatureCollection (type:'FeatureCollection', features:[...]). Required for choropleth (the regions); optional base map for bubble_map.",
          "additionalProperties": true
        },
        "topojson": {
          "type": "object",
          "description": "Inline TopoJSON Topology (type:'Topology'); converted to a GeoJSON FeatureCollection. Set 'topoObjectName' when the topology holds more than one object.",
          "additionalProperties": true
        },
        "topoObjectName": {
          "type": "string",
          "minLength": 1,
          "description": "Which TopoJSON object to extract (defaults to the sole object when there is exactly one)."
        },
        "rows": {
          "type": "array",
          "description": "Tabular records: for choropleth, the values joined into features (paired with 'join'); for bubble_map, the points to plot.",
          "items": {
            "type": "object",
            "additionalProperties": true
          },
          "maxItems": 5000
        },
        "join": {
          "type": "object",
          "description": "Choropleth join: merge each row into the feature whose 'featureProperty' equals the row's 'dataKey' value.",
          "required": [
            "dataKey",
            "featureProperty"
          ],
          "properties": {
            "dataKey": {
              "type": "string",
              "minLength": 1,
              "description": "Key in each tabular row."
            },
            "featureProperty": {
              "type": "string",
              "minLength": 1,
              "description": "Matching property name in each feature's properties."
            }
          },
          "additionalProperties": false
        },
        "valueField": {
          "type": "string",
          "minLength": 1,
          "description": "Choropleth: the numeric field — in feature properties, or merged in via 'join' — whose value colours each region. Required for choropleth."
        },
        "longitudeField": {
          "type": "string",
          "minLength": 1,
          "description": "Bubble map: the row field holding longitude. Required for bubble_map."
        },
        "latitudeField": {
          "type": "string",
          "minLength": 1,
          "description": "Bubble map: the row field holding latitude. Required for bubble_map."
        },
        "sizeField": {
          "type": "string",
          "minLength": 1,
          "description": "Bubble map: optional row field driving bubble size."
        },
        "colorField": {
          "type": "string",
          "minLength": 1,
          "description": "Bubble map: optional row field driving bubble colour."
        },
        "colorScale": {
          "type": "string",
          "enum": [
            "linear",
            "quantize",
            "quantile",
            "threshold",
            "ordinal",
            "diverging"
          ],
          "description": "Optional colour scale (defaults to a continuous linear ramp). 'ordinal' paints discrete per-category colours; 'diverging' ramps two hues about a neutral midpoint (zero)."
        },
        "originLongitudeField": {
          "type": "string",
          "minLength": 1,
          "description": "Flow map: the row field holding the origin longitude. Required for flow_map."
        },
        "originLatitudeField": {
          "type": "string",
          "minLength": 1,
          "description": "Flow map: the row field holding the origin latitude. Required for flow_map."
        },
        "destinationLongitudeField": {
          "type": "string",
          "minLength": 1,
          "description": "Flow map: the row field holding the destination longitude. Required for flow_map."
        },
        "destinationLatitudeField": {
          "type": "string",
          "minLength": 1,
          "description": "Flow map: the row field holding the destination latitude. Required for flow_map."
        },
        "strengthField": {
          "type": "string",
          "minLength": 1,
          "description": "Flow map: optional numeric row field whose magnitude drives each arc's line width (via a continuous visualMap)."
        },
        "curvature": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Flow map: optional arc curveness (0 = straight, ~0.3 default). Bends each origin→destination line into an arc."
        }
      },
      "additionalProperties": false
    },
    "hierarchyNode": {
      "type": "object",
      "description": "A nested-hierarchy node: a name, an optional numeric value, and optional children (recursive). Extra fields are preserved for tooltips.",
      "required": [
        "name"
      ],
      "properties": {
        "name": {
          "type": "string"
        },
        "value": {
          "type": "number"
        },
        "children": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/hierarchyNode"
          }
        }
      },
      "additionalProperties": true
    }
  }
}
