{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:akari-video:schema:review:v1",
  "title": "AKARI Video review.json v1 annotation model",
  "description": "QA ループの注釈サイドカー。edit.json と同じディレクトリに置く。時間アンカーは (src, source 秒) で永続化し、timeline 秒は永続化しない（contract-2026-07-18 §3/§4）。",
  "type": "object",
  "additionalProperties": true,
  "required": ["version", "annotations"],
  "properties": {
    "version": { "const": 0 },
    "annotations": {
      "type": "array",
      "items": { "$ref": "#/$defs/annotation" }
    }
  },
  "$defs": {
    "nonEmptyString": { "type": "string", "minLength": 1, "pattern": "\\S" },
    "sourceSeconds": { "type": "number", "minimum": 0 },
    "normalized01": { "type": "number", "minimum": 0, "maximum": 1 },
    "box": {
      "type": "array",
      "prefixItems": [
        { "$ref": "#/$defs/normalized01" },
        { "$ref": "#/$defs/normalized01" },
        { "$ref": "#/$defs/normalized01" },
        { "$ref": "#/$defs/normalized01" }
      ],
      "minItems": 4,
      "maxItems": 4,
      "$comment": "[x, y, w, h] 正規化座標。source フレーム基準（出力フレームではない）。意味制約 x+w<=1 かつ y+h<=1 は validate-review.mjs で検証する（analysis.schema.json の faceBox / edit.json crop.box と同一形式）。"
    },
    "strokePoint": {
      "type": "array",
      "prefixItems": [
        { "$ref": "#/$defs/normalized01" },
        { "$ref": "#/$defs/normalized01" }
      ],
      "minItems": 2,
      "maxItems": 2
    },
    "strokePoints": {
      "type": "array",
      "items": { "$ref": "#/$defs/strokePoint" },
      "minItems": 2,
      "$comment": "フリーハンド 1 ストロークの間引き済みポリライン（〜100 点程度。review セッション契約 §4.2）。"
    },
    "strokeFrame": {
      "type": "object",
      "additionalProperties": true,
      "required": ["sourceT"],
      "properties": {
        "sourceT": { "$ref": "#/$defs/sourceSeconds" },
        "cutIndex": { "oneOf": [{ "type": "null" }, { "type": "integer", "minimum": 0 }] }
      },
      "$comment": "描き始めフレーム（アンカー）。content-rect（動画面）のみ持つ。"
    },
    "stroke": {
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": true,
          "required": ["tool", "space", "frame", "points"],
          "properties": {
            "tool": { "const": "pen" },
            "space": { "const": "content-rect" },
            "frame": { "$ref": "#/$defs/strokeFrame" },
            "points": { "$ref": "#/$defs/strokePoints" },
            "sessionRef": { "$ref": "#/$defs/nonEmptyString" }
          },
          "$comment": "動画面。frame は必須。sessionRef は録音から作った線だけが持ち、録音なしでプレビューへ直接描いた線では省略する。"
        },
        {
          "type": "object",
          "additionalProperties": true,
          "required": ["tool", "space", "points"],
          "not": { "required": ["frame"] },
          "properties": {
            "tool": { "const": "pen" },
            "space": { "const": "image-rect" },
            "points": { "$ref": "#/$defs/strokePoints" },
            "sessionRef": { "$ref": "#/$defs/nonEmptyString" }
          },
          "$comment": "画像面（contract-2026-07-26-doc-image-annotations §3）。frame は動画フレームアンカーが存在しないため省略必須。sessionRef は録音セッション由来でない単発注釈では省略できる。"
        },
        {
          "type": "object",
          "additionalProperties": true,
          "required": ["tool", "space", "points"],
          "not": { "required": ["frame"] },
          "properties": {
            "tool": { "const": "pen" },
            "space": { "const": "canvas-rect" },
            "points": { "$ref": "#/$defs/strokePoints" },
            "canvasRef": { "$ref": "#/$defs/nonEmptyString" }
          },
          "$comment": "キャンバス面（contract-2026-07-26-canvas-surface §1/§4）。image-rect と同型だが出所参照フィールド名が canvasRef（c-0001/st-0003 形式）。frame は動画フレームアンカーが存在しないため省略必須。canvasRef は任意。"
        }
      ],
      "$comment": "review セッション契約 §4.2 の annotation 着地型（tool:pen 固定・space で判別）。"
    },
    "strokeReference": {
      "type": "object",
      "additionalProperties": true,
      "required": ["sessionId", "strokeId"],
      "properties": {
        "sessionId": { "type": "string", "pattern": "^s-[0-9]{4,}$" },
        "strokeId": { "type": "string", "pattern": "^st-[0-9]{4,}$" },
        "sessionRef": { "$ref": "#/$defs/nonEmptyString" }
      },
      "$comment": "review/sessions/<sessionId>/strokes.json 内の原本ストローク参照。pen/rect 共通で、strokes の点列埋め込みとは独立した追加フィールド。"
    },
    "refItem": {
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": true,
          "required": ["src"],
          "not": { "required": ["path"] },
          "properties": { "src": { "$ref": "#/$defs/nonEmptyString" } }
        },
        {
          "type": "object",
          "additionalProperties": true,
          "required": ["path"],
          "not": { "required": ["src"] },
          "properties": { "path": { "$ref": "#/$defs/nonEmptyString" } }
        }
      ],
      "$comment": "src = edit.json v1 sources[].id 参照。path = プロジェクト相対パス（copy-don't-link 後の実体）。1 エントリにつき排他。"
    },
    "sessionReference": {
      "type": "object",
      "additionalProperties": true,
      "required": ["id", "recRange", "confidence"],
      "properties": {
        "id": { "type": "string", "pattern": "^s-[0-9]{4,}$" },
        "recRange": {
          "type": "array",
          "prefixItems": [
            { "type": "number", "minimum": 0 },
            { "type": "number", "minimum": 0 }
          ],
          "minItems": 2,
          "maxItems": 2
        },
        "confidence": { "enum": ["high", "medium", "low"] }
      },
      "$comment": "review セッション契約 §6 の着地出所。id = review/sessions/<id>、recRange = 発話の録音秒 [start, end]、confidence = 位置解決の信頼度（compile-review-session は high / low を書き、low は本文に要確認が付く。medium は apps/shell の annotation-store.ts AnnotationSessionConfidence が受理する値で、読み手互換のため許容する）。"
    },
    "response": {
      "type": "object",
      "additionalProperties": true,
      "required": ["summary", "action", "respondedAt"],
      "properties": {
        "summary": { "type": "string" },
        "action": { "enum": ["edited", "declined"] },
        "respondedAt": { "type": "string" }
      }
    },
    "annotation": {
      "type": "object",
      "additionalProperties": true,
      "required": ["id", "createdAt", "sourceT", "text", "input", "status"],
      "properties": {
        "id": { "$ref": "#/$defs/nonEmptyString" },
        "createdAt": { "type": "string" },
        "src": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/nonEmptyString" }],
          "$comment": "edit.json v1 の sources[].id 参照。省略/null = 単一ソース互換。参照整合は edit-lint が検証する。"
        },
        "sourceT": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/sourceSeconds" }],
          "$comment": "source 秒（cuts[].in/out と同一座標系）。timeline 秒ではない。null は target が doc:<path>#<block-id> / image:<path> のときのみ許容する（contract-2026-07-26-doc-image-annotations §2）。動画面の注釈（target が overlay: / cut: / null）は従来どおり必須 — validate-review.mjs / createAnnotation が実体を検証する。"
        },
        "sourceRange": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "array",
              "prefixItems": [
                { "$ref": "#/$defs/sourceSeconds" },
                { "$ref": "#/$defs/sourceSeconds" }
              ],
              "minItems": 2,
              "maxItems": 2
            }
          ],
          "$comment": "[start, end) source 秒。start < end は validate-review.mjs で検証する。"
        },
        "timelineT": {
          "oneOf": [{ "type": "null" }, { "type": "number" }],
          "deprecated": true,
          "$comment": "非推奨（contract-2026-07-18 §3 違反の旧フィールド）。新規書き込みは常に null。timeline 位置は表示のたびに cuts[] から射影する。"
        },
        "target": {
          "oneOf": [{ "type": "null" }, { "type": "string" }],
          "$comment": "targetKind とは別物。null / overlay:<id> / cut:<index> に加え、contract-2026-07-26-doc-image-annotations §1 で doc:<プロジェクト相対パス>#<block-id>（分析レポート等のブロック注釈）と image:<プロジェクト相対パス>（画像アセットへの注釈）を、contract-2026-07-26-canvas-surface §4 で canvas:<c-NNNN>（キャンバス面への注釈）を追加する。doc: / image: / canvas: のときに限り sourceT は null を許容する（§2 および同契約 §4）。"
        },
        "targetKind": {
          "oneOf": [
            { "type": "null" },
            { "enum": ["instant", "range", "region", "asset", "insert"] }
          ],
          "$comment": "対象 5 型の判別子。null/省略 = 旧レコード（sourceRange の有無で instant/range を解釈）。型ごとの期待フィールド（range→sourceRange、region→region|strokes、asset→refs、insert→insertPosition）は助言レベル（validate-review.mjs / edit-lint の warning）。"
        },
        "region": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": true,
              "required": ["box"],
              "properties": { "box": { "$ref": "#/$defs/box" } }
            }
          ]
        },
        "strokes": {
          "oneOf": [
            { "type": "null" },
            { "type": "array", "items": { "$ref": "#/$defs/stroke" }, "minItems": 1 }
          ],
          "$comment": "review セッション契約 §4.2 で確定した着地型（tool/space/points 等を持つオブジェクト配列）。region.box と両方非 null の場合は region.box が勝つ（warning）。"
        },
        "strokeRefs": {
          "oneOf": [
            { "type": "null" },
            { "type": "array", "items": { "$ref": "#/$defs/strokeReference" }, "minItems": 1 }
          ],
          "$comment": "セッション原本への追加的な参照。欠落/null は従来データとして正常。"
        },
        "refs": {
          "oneOf": [
            { "type": "null" },
            { "type": "array", "items": { "$ref": "#/$defs/refItem" }, "minItems": 1 }
          ]
        },
        "insertPosition": {
          "oneOf": [{ "type": "null" }, { "enum": ["before", "after"] }],
          "$comment": "targetKind insert 用。アンカー (src, sourceT) に対応する timeline 位置の前/後に挿入する。"
        },
        "intent": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/nonEmptyString" }],
          "$comment": "自由文字列。推奨語彙は契約文書（enum 強制しない）。"
        },
        "text": { "type": "string" },
        "input": {
          "enum": ["typed", "voice", "session"],
          "$comment": "review セッション契約 §6 で追加された着地元。ライダー r1（2026-07-26-canvas-surface 起票時点で回収）: この enum と apps/shell reader は当初 session 未許可のままだったため実データ（skills/address-review/dev-fixtures/fixture-project/review.json）が validator エラーになっていた。"
        },
        "audio": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/nonEmptyString" }]
        },
        "transcript": {
          "oneOf": [{ "type": "null" }, { "type": "string" }],
          "$comment": "発話原文（review セッション契約 §6）。text は正規化済み指示文、transcript は聴感の温度を残す原文。session 由来以外では null / 省略。"
        },
        "session": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/sessionReference" }],
          "$comment": "review セッションからの着地の出所参照（review セッション契約 §6）。input: session のときに compile-review-session が書く。省略 / null は従来データとして正常。"
        },
        "poses": {
          "type": "null",
          "$comment": "予約のまま（実演キャプチャ。本契約のスコープ外）。"
        },
        "status": { "enum": ["open", "addressed", "resolved"] },
        "response": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/response" }]
        }
      }
    }
  }
}
