{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://vclaw.dev/schemas/video/artifacts/shot-table.schema.json",
  "title": "ShotTableArtifact",
  "description": "The 3d-animation-short shot table: one row per shot, carrying per-second beats, the spatial anchor chain, the continuity handoff and the audio track. It is the CHECKABLE contract the six-check gate runs against (skills/3d-animation-short/scripts/check_shot_table.py), and it composes DOWN into route-shaped prompts. It is not an obedience contract: no route honours one-second-granularity choreography.",
  "type": "object",
  "required": ["schemaVersion", "projectSlug", "generatedAt", "shots"],
  "additionalProperties": false,
  "properties": {
    "schemaVersion": { "const": 1 },
    "projectSlug": { "type": "string", "minLength": 1 },
    "generatedAt": { "type": "string", "format": "date-time" },
    "title": { "type": "string" },
    "styleLockId": {
      "type": "string",
      "description": "Style id in the engine's style-lock register. Every artifact of the film shares it."
    },
    "aspectRatio": { "type": "string" },
    "totalDurationSeconds": { "type": "number", "exclusiveMinimum": 0 },
    "dialogueMode": {
      "enum": ["none", "vo", "dialogue"],
      "description": "Never inferred. 'none' unless the operator asked for speech; language is never defaulted."
    },
    "dialogueLanguage": {
      "type": "string",
      "description": "Set ONLY when the operator stated a language. Absent means unspecified, not English."
    },
    "route": {
      "type": "string",
      "description": "The film's render route. One film, one route — a per-shot override leaves a visible style seam."
    },
    "shots": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/definitions/Shot" }
    },
    "selfCheck": { "$ref": "#/definitions/SelfCheckStamp" }
  },
  "definitions": {
    "Shot": {
      "type": "object",
      "description": "The schema describes the SHAPE of a row. COMPLETENESS is enforced by check_shot_table.py, not here: make_shot_table.py emits a scaffold whose creative fields are deliberately blank, and that scaffold must still be a valid artifact so it can be written, read back and edited. `hookType` is therefore optional here and required by the gate.",
      "required": ["id", "durationSeconds", "beats"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^S[0-9]{2,3}$",
          "description": "Shot id, e.g. S01. Ordering is the array order; the id is the stable handle."
        },
        "title": { "type": "string" },
        "durationSeconds": { "type": "number", "exclusiveMinimum": 0 },
        "hookType": {
          "description": "Blank in a scaffold; the gate refuses a blank one. Empty string is allowed only so the scaffold round-trips.",
          "enum": [
            "",
            "setup",
            "visual-joke",
            "reversal",
            "reveal",
            "callback",
            "suspense",
            "tender",
            "chase",
            "expression-beat",
            "climax"
          ]
        },
        "scene": {
          "type": "string",
          "description": "Environment plate name, bound by exact name to artifacts/environment-assets.json."
        },
        "characters": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Exact cast names, bound to the registered identity for the chosen route."
        },
        "performanceId": {
          "description": "Animation-acting register from the engine's performance register. Absent falls back to a guess made from hookType — and a hook is a STORY function, not a body mechanic, so the guess is often wrong (a reveal of an OBJECT got turn-of-the-head direction). Set it per shot whenever the body mechanics matter.",
          "enum": ["elastic-broad", "grounded-warm", "frantic-chase", "held-still", "impact-recoil", "reveal-turn"]
        },
        "continuity": { "$ref": "#/definitions/Continuity" },
        "anchors": { "$ref": "#/definitions/Anchors" },
        "beats": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/definitions/Beat" }
        },
        "audioTrack": { "$ref": "#/definitions/AudioTrack" },
        "route": {
          "type": "string",
          "description": "Per-shot route override. Deliberate exception only — mixing routes inside a film shows as a style seam."
        },
        "notes": { "type": "string" }
      }
    },
    "Continuity": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "from": {
          "type": "string",
          "description": "How this shot continues the previous shot's ending image, prop position, eyeline, posture, sound bridge or emotional state."
        },
        "to": {
          "type": "string",
          "description": "What state this shot leaves for the next shot's opening."
        },
        "hardCut": {
          "type": "boolean",
          "description": "True when this shot deliberately contradicts the previous ending (time skip, location jump). Required to pass the continuity-chain check when state flips."
        },
        "hardCutReason": { "type": "string" }
      }
    },
    "Anchors": {
      "type": "object",
      "description": "The spatial anchor chain. This is what makes drift auditable after a render instead of a matter of opinion.",
      "additionalProperties": false,
      "properties": {
        "landmarks": {
          "type": "array",
          "items": { "$ref": "#/definitions/Landmark" },
          "description": "Fixed objects from the environment plate whose screen position must persist across shots in the same scene."
        },
        "characterPositions": {
          "type": "array",
          "items": { "$ref": "#/definitions/CharacterPosition" }
        },
        "exited": {
          "type": "array",
          "items": { "$ref": "#/definitions/ExitedCharacter" },
          "description": "Anyone on screen in the previous shot but not this one. Tracked for at least one shot, then dropped after two consecutive shots off stage."
        },
        "lighting": { "$ref": "#/definitions/LightingBaseline" },
        "continuityNote": {
          "type": "string",
          "description": "Explicit note when landmarks or lighting legitimately change within a scene, e.g. a camera orbit moving the door frame across the frame."
        }
      }
    },
    "Landmark": {
      "type": "object",
      "required": ["name", "position"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "position": {
          "type": "string",
          "minLength": 1,
          "description": "Screen-relative, e.g. 'right third', 'centre bottom'."
        }
      }
    },
    "CharacterPosition": {
      "type": "object",
      "description": "Scaffolded blank, one per cast member, as a template for the author. A landmark is never scaffolded, which is why Landmark demands a position and this does not.",
      "required": ["name"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "position": {
          "type": "string",
          "description": "Screen-relative in camera view: left/centre/right, top/mid/bottom, foreground/midground/background."
        },
        "facing": { "type": "string" },
        "pose": { "type": "string" }
      }
    },
    "ExitedCharacter": {
      "type": "object",
      "required": ["name"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "lastSeen": { "type": "string" },
        "reason": { "type": "string" }
      }
    },
    "LightingBaseline": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "key": { "type": "string" },
        "fill": { "type": "string" },
        "rim": { "type": "string" },
        "modifier": { "type": "string" }
      }
    },
    "Beat": {
      "type": "object",
      "description": "One per-second (or sub-second) directive. All five elements are required by check_shot_table.py; they are optional HERE so a scaffold with blank elements is still a valid artifact. An intentionally quiet beat writes audio: 'silent' — the gate tells that apart from an omission.",
      "required": ["from", "to"],
      "additionalProperties": false,
      "properties": {
        "from": { "type": "number", "minimum": 0 },
        "to": { "type": "number", "exclusiveMinimum": 0 },
        "action": {
          "type": "string",
          "description": "Action, pose and expression — squash/stretch, anticipation, overshoot, follow-through where they apply."
        },
        "camera": {
          "type": "string",
          "description": "Shot size and movement: push, pull, pan, tilt, handheld, locked, orbit; Dutch angle when designed."
        },
        "spatial": {
          "type": "string",
          "description": "Where the character is, what they hold, which landmark is in frame."
        },
        "audio": {
          "type": "string",
          "description": "Narration, dialogue, SFX, breath — or the literal 'silent' when the silence is intended."
        },
        "handoff": {
          "type": "string",
          "description": "The state this beat locks in for the next beat or the next shot."
        },
        "mouth": {
          "enum": ["open", "closed"],
          "description": "Required reading when a character speaks in this beat. Off-screen narration defaults closed; on-screen dialogue defaults open."
        },
        "critical": {
          "type": "boolean",
          "description": "The shot's hook lands on this beat."
        }
      }
    },
    "AudioTrack": {
      "type": "object",
      "description": "The shot's audio script in time order, separate from the per-beat cues.",
      "additionalProperties": false,
      "properties": {
        "narration": {
          "type": "array",
          "items": { "$ref": "#/definitions/NarrationLine" }
        },
        "dialogue": {
          "type": "array",
          "items": { "$ref": "#/definitions/DialogueLine" }
        },
        "sfx": {
          "type": "array",
          "items": { "$ref": "#/definitions/SfxCue" }
        },
        "performanceNote": { "type": "string" }
      }
    },
    "NarrationLine": {
      "type": "object",
      "required": ["text"],
      "additionalProperties": false,
      "properties": {
        "text": { "type": "string", "minLength": 1 },
        "from": { "type": "number", "minimum": 0 },
        "to": { "type": "number", "exclusiveMinimum": 0 }
      }
    },
    "DialogueLine": {
      "type": "object",
      "required": ["speaker", "line"],
      "additionalProperties": false,
      "properties": {
        "speaker": { "type": "string", "minLength": 1 },
        "line": { "type": "string", "minLength": 1 },
        "tone": { "type": "string" },
        "from": { "type": "number", "minimum": 0 },
        "to": { "type": "number", "exclusiveMinimum": 0 }
      }
    },
    "SfxCue": {
      "type": "object",
      "required": ["cue"],
      "additionalProperties": false,
      "properties": {
        "cue": { "type": "string", "minLength": 1 },
        "at": { "type": "number", "minimum": 0 }
      }
    },
    "SelfCheckStamp": {
      "type": "object",
      "description": "Written by check_shot_table.py only when all six checks pass. Its absence is what blocks storyboarding.",
      "required": ["passed", "checkedAt"],
      "additionalProperties": false,
      "properties": {
        "passed": { "type": "boolean" },
        "checkedAt": { "type": "string", "format": "date-time" },
        "checks": {
          "type": "array",
          "items": { "$ref": "#/definitions/CheckResult" }
        }
      }
    },
    "CheckResult": {
      "type": "object",
      "required": ["id", "passed"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "enum": [
            "hook-density",
            "shot-duration",
            "cast-per-shot",
            "anchor-inheritance",
            "beat-coverage",
            "continuity-chain"
          ]
        },
        "passed": { "type": "boolean" },
        "failures": { "type": "array", "items": { "type": "string" } }
      }
    }
  }
}
