{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://map.viguie.info/journey-package.schema.json",
  "title": "HistoriEarth local journey package v3",
  "description": "Validate structure with this schema, then use the browser importer for cross-record chronology, evidence references, narration/caption rules, canonical URL uniqueness and actual local asset checks. Passing either check does not verify historical accuracy.",
  "type": "object",
  "additionalProperties": false,
  "required": ["format", "title", "locale", "centralSubject", "sources", "chapters"],
  "properties": {
    "format": { "const": "historiearth-local-journey/v3" },
    "title": { "type": "string", "minLength": 1, "maxLength": 180 },
    "locale": { "description": "Content language for transcripts, captions and browser narration, independent of the interface language.", "type": "string", "pattern": "^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$", "maxLength": 35 },
    "centralSubject": { "type": "string", "minLength": 8, "maxLength": 180 },
    "sources": { "type": "array", "minItems": 1, "maxItems": 80, "items": { "$ref": "#/$defs/source" } },
    "chapters": { "description": "Choose 2–18 chapters from the subject's distinct historical, causal and geographic structure. There is no preferred count or threshold at six or seven. Narrative order: startYear must never decrease; multiple chapters may start in the same year and retain array order. Do not invent years to separate chapters.", "type": "array", "minItems": 2, "maxItems": 18, "items": { "$ref": "#/$defs/chapter" } }
  },
  "$defs": {
    "sourceId": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 100 },
    "sourceIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } },
    "source": {
      "type": "object", "additionalProperties": false, "required": ["id", "label", "url", "roles", "locator", "supportSummary", "limitations"],
      "properties": {
        "id": { "$ref": "#/$defs/sourceId" }, "label": { "type": "string", "minLength": 1, "maxLength": 240 },
        "url": { "type": "string", "format": "uri", "pattern": "^https://", "maxLength": 2000 },
        "roles": { "type": "array", "minItems": 1, "maxItems": 6, "uniqueItems": true, "items": { "enum": ["identity-orientation", "chronology-primary-record", "scholarly-interpretation", "materially-different-perspective", "geography", "media-rights"] } },
        "locator": { "type": "string", "minLength": 3, "maxLength": 300 }, "supportSummary": { "type": "string", "minLength": 20, "maxLength": 600 },
        "limitations": { "type": "string", "minLength": 12, "maxLength": 500 }
      }
    },
    "claim": {
      "type": "object", "additionalProperties": false, "required": ["id", "text", "sourceIds"],
      "properties": { "id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 120 }, "text": { "type": "string", "minLength": 20, "maxLength": 600 }, "sourceIds": { "$ref": "#/$defs/sourceIds" } }
    },
    "location": {
      "$ref": "#/$defs/place"
    },
    "marker": {
      "$ref": "#/$defs/place"
    },
    "place": {
      "type": "object", "additionalProperties": false,
      "required": ["id", "label", "type", "sourceIds", "claimIds", "supportSummary"],
      "properties": {
        "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,79}$" }, "label": { "type": "string", "minLength": 1, "maxLength": 180 },
        "type": { "enum": ["city", "port", "island", "strait", "region", "country", "other"] }, "countryCode": { "type": "string", "pattern": "^[A-Z]{2}$" },
        "region": { "type": "string", "minLength": 1, "maxLength": 180 }, "aliases": { "type": "array", "maxItems": 8, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 180 } },
        "historicalPeriod": { "type": "string", "minLength": 1, "maxLength": 120 }, "authorityIds": { "type": "array", "maxItems": 4, "uniqueItems": true, "items": { "type": "string", "pattern": "^(geonames:[0-9]+|wikidata:Q[0-9]+)$" } },
        "longitude": { "type": "number", "minimum": -180, "maximum": 180 }, "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
        "coordinatePrecision": { "type": "string", "minLength": 1, "maxLength": 80 }, "cameraHeight": { "type": "integer", "minimum": 250000, "maximum": 20000000 },
        "sourceIds": { "$ref": "#/$defs/sourceIds" }, "claimIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } },
        "supportSummary": { "type": "string", "minLength": 20, "maxLength": 500 }
      },
      "allOf": [
        { "if": { "required": ["longitude"] }, "then": { "required": ["latitude", "coordinatePrecision"] } },
        { "if": { "required": ["latitude"] }, "then": { "required": ["longitude", "coordinatePrecision"] } }
      ]
    },
    "sensitivity": {
      "type": "object", "additionalProperties": false, "allOf": [{"if": {"properties": {"responsibleActorsEstablished": {"const": true}}, "required": ["responsibleActorsEstablished"]}, "then": {"properties": {"responsibleActors": {"type": "array", "minItems": 1}}}, "else": {"properties": {"responsibleActors": {"type": "array", "maxItems": 0}}}}], "required": ["affectedGroups", "responsibleActorsEstablished", "responsibleActors", "coercionOrViolence", "scale", "representationCaveats", "sourceIds"],
      "properties": {
        "affectedGroups": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "type": "string", "minLength": 1, "maxLength": 180 } },
        "responsibleActorsEstablished": { "type": "boolean" }, "responsibleActors": { "type": "array", "maxItems": 8, "items": { "type": "string", "minLength": 1, "maxLength": 180 } },
        "coercionOrViolence": { "type": "string", "minLength": 20, "maxLength": 500 }, "scale": { "type": "string", "minLength": 20, "maxLength": 500 },
        "representationCaveats": { "type": "string", "minLength": 20, "maxLength": 500 }, "sourceIds": { "$ref": "#/$defs/sourceIds" }
      }
    },
    "mediaResearch": {
      "description": "Author-reported research, not independently verified. Image counts are derived from the chapter media array.",
      "type": "object", "additionalProperties": false, "required": ["status", "note"],
      "properties": {
        "status": { "enum": ["completed", "blocked", "not-started"] },
        "note": { "type": "string", "minLength": 1, "maxLength": 600, "pattern": "\\S" }
      }
    },
    "media": {
      "type": "object", "additionalProperties": false,
      "required": ["purpose", "caption", "creator", "date", "attribution", "sourceUrl", "licence", "rightsStatus", "approved", "relevance", "distinctContribution"],
      "oneOf": [{ "required": ["file"], "not": { "required": ["url"] } }, { "required": ["url"], "not": { "required": ["file"] } }],
      "properties": {
        "file": { "type": "string", "pattern": "^media/(?!.*\\.\\.)(?!.*//)[a-zA-Z0-9][a-zA-Z0-9._/-]*$", "maxLength": 240 }, "url": { "type": "string", "format": "uri", "pattern": "^https://", "maxLength": 2000 },
        "purpose": { "const": "chapter-display" }, "mimeType": { "enum": ["image/jpeg", "image/png", "image/webp"] }, "byteLength": { "type": "integer", "minimum": 1, "maximum": 20971520 },
        "width": { "type": "integer", "minimum": 1, "maximum": 16384 }, "height": { "type": "integer", "minimum": 1, "maximum": 16384 }, "caption": { "type": "string", "minLength": 20, "maxLength": 260 },
        "creator": { "type": "string", "minLength": 1, "maxLength": 300 }, "date": { "type": "string", "minLength": 1, "maxLength": 180 }, "attribution": { "type": "string", "minLength": 1, "maxLength": 700 },
        "sourceUrl": { "type": "string", "format": "uri", "pattern": "^https://", "maxLength": 2000 }, "licence": { "type": "string", "minLength": 1, "maxLength": 180 },
        "rightsStatus": { "enum": ["reusable", "personal-private-use"] }, "approved": { "const": true }, "relevance": { "type": "string", "minLength": 20, "maxLength": 600 }, "distinctContribution": { "const": true }
      }
    },
    "audio": { "type": "object", "additionalProperties": false, "required": ["file", "byteLength"], "properties": { "file": { "type": "string", "pattern": "^audio/(?!.*\\.\\.)(?!.*//)[a-zA-Z0-9][a-zA-Z0-9._/-]*\\.[mM][pP]3$", "maxLength": 240 }, "byteLength": { "type": "integer", "minimum": 1, "maximum": 26214400 } } },
    "mapDate": {
      "type": "object", "additionalProperties": false, "required": ["id", "position", "label", "precision", "sourceIds", "claimIds"],
      "properties": { "id": { "$ref": "#/$defs/sourceId" }, "position": { "type": "number", "minimum": -10000000, "maximum": 10000000 }, "label": { "type": "string", "minLength": 1, "maxLength": 120 }, "precision": { "enum": ["day", "month", "year", "approximate"] }, "sourceIds": { "$ref": "#/$defs/sourceIds" }, "claimIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } } }
    },
    "movingFigure": {
      "type": "object", "additionalProperties": false, "required": ["kind", "label", "sourceIds", "claimIds"],
      "properties": { "kind": { "const": "boat" }, "label": { "type": "string", "minLength": 1, "maxLength": 120 }, "sourceIds": { "$ref": "#/$defs/sourceIds" }, "claimIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } } }
    },
    "route": {
      "type": "object", "additionalProperties": false, "required": ["id", "label", "representation", "stopIds", "timing", "startDateId", "endDateId", "sourceIds", "claimIds"],
      "properties": {
        "id": { "$ref": "#/$defs/sourceId" }, "label": { "type": "string", "minLength": 1, "maxLength": 180 }, "representation": { "enum": ["sourced-corridor", "schematic-itinerary"] },
        "coordinates": { "type": "array", "minItems": 2, "maxItems": 4096, "items": { "type": "array", "prefixItems": [{ "type": "number", "minimum": -180, "maximum": 180 }, { "type": "number", "minimum": -90, "maximum": 90 }], "items": false, "minItems": 2, "maxItems": 2 } },
        "pathPrecision": { "type": "string", "minLength": 12, "maxLength": 160, "pattern": "^[^<>\\u0000-\\u0008]+$" }, "derivation": { "type": "string", "minLength": 20, "maxLength": 500, "pattern": "^[^<>\\u0000-\\u0008]+$" },
        "stopIds": { "type": "array", "minItems": 2, "maxItems": 16, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } }, "timing": { "enum": ["sourced", "estimated", "illustrative"] },
        "startDateId": { "$ref": "#/$defs/sourceId" }, "endDateId": { "$ref": "#/$defs/sourceId" }, "sourceIds": { "$ref": "#/$defs/sourceIds" }, "claimIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } },
        "movingFigure": { "$ref": "#/$defs/movingFigure" }, "initiallyVisible": { "type": "boolean" }
      },
      "allOf": [
        { "if": { "properties": { "representation": { "const": "sourced-corridor" } }, "required": ["representation"] }, "then": { "required": ["coordinates", "pathPrecision", "derivation"] } },
        { "if": { "properties": { "representation": { "const": "schematic-itinerary" } }, "required": ["representation"] }, "then": { "not": { "anyOf": [{ "required": ["coordinates"] }, { "required": ["movingFigure"] }, { "required": ["pathPrecision"] }, { "required": ["derivation"] }] } } }
      ]
    },
    "geometry": {
      "type": "object", "additionalProperties": false, "required": ["type", "coordinates"],
      "properties": { "type": { "enum": ["Polygon", "MultiPolygon"] }, "coordinates": { "type": "array", "minItems": 1 } }
    },
    "areaStyle": {
      "type": "object", "additionalProperties": false, "required": ["fill", "fillOpacity", "border", "borderOpacity", "borderWidth", "state"],
      "properties": { "fill": { "enum": ["teal", "amber", "crimson", "violet", "blue", "grey"] }, "fillOpacity": { "type": "number", "minimum": 0, "maximum": 0.7 }, "border": { "enum": ["teal", "amber", "crimson", "violet", "blue", "grey"] }, "borderOpacity": { "type": "number", "minimum": 0.2, "maximum": 1 }, "borderWidth": { "type": "number", "minimum": 1, "maximum": 6 }, "state": { "enum": ["active", "reached", "selected"] } }
    },
    "area": {
      "type": "object", "additionalProperties": false, "required": ["id", "label", "dateId", "sourceIds", "claimIds", "confidence", "viewpoint", "interpretation", "boundaryPrecision", "validity", "style"],
      "oneOf": [{ "required": ["geometry"], "not": { "required": ["geometryFile"] } }, { "required": ["geometryFile"], "not": { "required": ["geometry"] } }],
      "properties": { "id": { "$ref": "#/$defs/sourceId" }, "label": { "type": "string", "minLength": 1, "maxLength": 180 }, "geometry": { "$ref": "#/$defs/geometry" }, "geometryFile": { "type": "string", "pattern": "^geometry/(?!.*\\.\\.)(?!.*//)[a-zA-Z0-9][a-zA-Z0-9._/-]*\\.geojson$", "maxLength": 240 }, "dateId": { "$ref": "#/$defs/sourceId" }, "sourceIds": { "$ref": "#/$defs/sourceIds" }, "claimIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } }, "confidence": { "type": "string", "minLength": 1, "maxLength": 120 }, "viewpoint": { "type": "string", "minLength": 1, "maxLength": 240 }, "interpretation": { "type": "string", "minLength": 1, "maxLength": 500 }, "boundaryPrecision": { "type": "string", "minLength": 1, "maxLength": 120 }, "validity": { "type": "string", "minLength": 1, "maxLength": 120 }, "style": { "$ref": "#/$defs/areaStyle" }, "initiallyVisible": { "type": "boolean" } }
    },
    "action": {
      "description": "One exact portable-v3 action object. The examples enumerate every supported action type; they are fragments for chapters[].map.beats[].action or, where permitted, supporting[]. Do not invent aliases or a generic command language.",
      "type": "object", "additionalProperties": false, "required": ["type", "targetIds", "durationMs"],
      "properties": { "type": { "enum": ["camera-frame", "camera-traverse", "timeline-set", "timeline-advance", "marker-reveal", "marker-focus", "route-trace", "area-reveal", "area-hide", "area-emphasis", "area-transition", "staged-area-sequence", "media-cue"] }, "targetIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } }, "durationMs": { "type": "integer", "minimum": 0, "maximum": 120000 } },
      "allOf": [{ "if": { "type": "object", "properties": { "type": { "enum": ["area-transition", "staged-area-sequence"] } }, "required": ["type"] }, "then": { "properties": { "targetIds": { "type": "array", "minItems": 2 } } }, "else": { "if": { "type": "object", "properties": { "type": { "const": "marker-reveal" } }, "required": ["type"] }, "else": { "properties": { "targetIds": { "type": "array", "maxItems": 1 } } } } }],
      "examples": [
        { "type": "camera-frame", "targetIds": ["chapter-focus"], "durationMs": 1500 },
        { "type": "camera-traverse", "targetIds": ["sourced-route"], "durationMs": 8000 },
        { "type": "timeline-set", "targetIds": ["date-start"], "durationMs": 0 },
        { "type": "timeline-advance", "targetIds": ["date-end"], "durationMs": 3000 },
        { "type": "marker-reveal", "targetIds": ["first-marker", "second-marker"], "durationMs": 500 },
        { "type": "marker-focus", "targetIds": ["first-marker"], "durationMs": 1000 },
        { "type": "route-trace", "targetIds": ["sourced-route"], "durationMs": 12000 },
        { "type": "area-reveal", "targetIds": ["area-before"], "durationMs": 1000 },
        { "type": "area-hide", "targetIds": ["area-before"], "durationMs": 1000 },
        { "type": "area-emphasis", "targetIds": ["area-after"], "durationMs": 750 },
        { "type": "area-transition", "targetIds": ["area-before", "area-after"], "durationMs": 2000 },
        { "type": "staged-area-sequence", "targetIds": ["area-before", "area-middle", "area-after"], "durationMs": 4000 },
        { "type": "media-cue", "targetIds": ["media-1"], "durationMs": 0 }
      ]
    },
    "supportingAction": {
      "allOf": [{ "$ref": "#/$defs/action" }, { "type": "object", "properties": { "type": { "enum": ["timeline-set", "marker-reveal", "marker-focus", "area-reveal", "area-hide", "area-emphasis", "media-cue"] } }, "required": ["type"] }]
    },
    "beat": {
      "type": "object", "additionalProperties": false, "required": ["id", "narration", "claimIds", "action", "supporting", "fallbackDurationMs"],
      "properties": { "id": { "$ref": "#/$defs/sourceId" }, "narration": { "type": "string", "minLength": 1, "maxLength": 4000 }, "claimIds": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/sourceId" } }, "action": { "anyOf": [{ "$ref": "#/$defs/action" }, { "type": "null" }] }, "supporting": { "type": "array", "maxItems": 4, "items": { "$ref": "#/$defs/supportingAction" } }, "fallbackDurationMs": { "type": "integer", "minimum": 500, "maximum": 120000 }, "motionPurpose": { "type": "string", "minLength": 20, "maxLength": 500 }, "framingRisks": { "type": "string", "minLength": 20, "maxLength": 500 } }
    },
    "map": {
      "description": "Exact chapter-level container for portable animation declarations. The example demonstrates a complete inline territorial transition; replace its placeholder source and claim IDs with IDs declared by the package and chapter.",
      "type": "object", "additionalProperties": false, "required": ["dates", "routes", "areas", "beats"],
      "properties": { "dates": { "type": "array", "maxItems": 24, "items": { "$ref": "#/$defs/mapDate" } }, "routes": { "type": "array", "maxItems": 12, "items": { "$ref": "#/$defs/route" } }, "areas": { "type": "array", "maxItems": 16, "items": { "$ref": "#/$defs/area" } }, "beats": { "type": "array", "minItems": 1, "maxItems": 24, "items": { "$ref": "#/$defs/beat" } } },
      "examples": [{
        "dates": [
          { "id": "date-before", "position": 1772, "label": "1772", "precision": "year", "sourceIds": ["source-id"], "claimIds": ["claim-id"] },
          { "id": "date-after", "position": 1793, "label": "1793", "precision": "year", "sourceIds": ["source-id"], "claimIds": ["claim-id"] }
        ],
        "routes": [],
        "areas": [
          { "id": "area-before", "label": "Sourced extent before the change", "geometry": { "type": "Polygon", "coordinates": [[[18, 50], [24, 50], [24, 55], [18, 55], [18, 50]]] }, "dateId": "date-before", "sourceIds": ["source-id"], "claimIds": ["claim-id"], "confidence": "Illustrative schema example", "viewpoint": "A reader-facing territorial overview", "interpretation": "This placeholder polygon demonstrates structure only and is not historical evidence.", "boundaryPrecision": "Placeholder geometry for schema demonstration", "validity": "Replace with the source-supported historical validity period", "style": { "fill": "teal", "fillOpacity": 0.3, "border": "teal", "borderOpacity": 0.9, "borderWidth": 2, "state": "active" }, "initiallyVisible": false },
          { "id": "area-after", "label": "Sourced extent after the change", "geometry": { "type": "Polygon", "coordinates": [[[19, 51], [23, 51], [23, 54], [19, 54], [19, 51]]] }, "dateId": "date-after", "sourceIds": ["source-id"], "claimIds": ["claim-id"], "confidence": "Illustrative schema example", "viewpoint": "A reader-facing territorial overview", "interpretation": "This placeholder polygon demonstrates structure only and is not historical evidence.", "boundaryPrecision": "Placeholder geometry for schema demonstration", "validity": "Replace with the source-supported historical validity period", "style": { "fill": "amber", "fillOpacity": 0.35, "border": "amber", "borderOpacity": 0.9, "borderWidth": 2, "state": "active" }, "initiallyVisible": false }
        ],
        "beats": [
          { "id": "beat-before", "narration": "The narration segment associated with the first sourced territorial state.", "claimIds": ["claim-id"], "action": { "type": "area-reveal", "targetIds": ["area-before"], "durationMs": 1000 }, "supporting": [{ "type": "timeline-set", "targetIds": ["date-before"], "durationMs": 0 }], "fallbackDurationMs": 3000, "motionPurpose": "The first discrete reveal establishes the earlier sourced territorial state.", "framingRisks": "The placeholder polygon must be replaced and must not be presented as historical evidence." },
          { "id": "beat-after", "narration": "The next narration segment explains the documented change to the later territorial state.", "claimIds": ["claim-id"], "action": { "type": "area-transition", "targetIds": ["area-before", "area-after"], "durationMs": 2000 }, "supporting": [{ "type": "timeline-set", "targetIds": ["date-after"], "durationMs": 0 }], "fallbackDurationMs": 3000, "motionPurpose": "The discrete transition distinguishes two separately sourced territorial states.", "framingRisks": "The transition must not imply boundary morphing, exact speed, or an unsupported continuous process." }
        ]
      }]
    },
    "chapter": {
      "type": "object", "additionalProperties": false, "required": ["id", "startYear", "title", "subjectConnection", "transcript", "location", "claims", "harmSensitive"],
      "properties": {
        "id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 100 }, "startYear": { "type": "integer", "minimum": -10000000, "maximum": 10000000 }, "endYear": { "type": "integer", "minimum": -10000000, "maximum": 10000000 },
        "title": { "type": "string", "minLength": 1, "maxLength": 180 }, "subjectConnection": { "type": "string", "minLength": 20, "maxLength": 400 }, "transcript": { "type": "string", "minLength": 650, "maxLength": 4000 },
        "location": { "$ref": "#/$defs/location" }, "markers": { "type": "array", "maxItems": 5, "items": { "$ref": "#/$defs/marker" } }, "claims": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "$ref": "#/$defs/claim" } },
        "harmSensitive": { "type": "boolean" }, "sensitivity": { "$ref": "#/$defs/sensitivity" }, "mediaResearch": { "$ref": "#/$defs/mediaResearch" }, "media": { "type": "array", "maxItems": 6, "items": { "$ref": "#/$defs/media" } }, "audio": { "$ref": "#/$defs/audio" }, "map": { "$ref": "#/$defs/map" }
      },
      "allOf": [
        { "if": { "properties": { "harmSensitive": { "const": true } }, "required": ["harmSensitive"] }, "then": { "required": ["sensitivity"] } },
        { "if": { "properties": { "harmSensitive": { "const": false } }, "required": ["harmSensitive"] }, "then": { "not": { "required": ["sensitivity"] } } }
      ]
    }
  }
}
