{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://hai.ai/schemas/benchmark-scenario/v1/benchmark-scenario.schema.json",
  "title": "HAI.AI Benchmark Scenario (public contribution) v1",
  "description": "Public, contributor-facing schema for a HAI.AI two-party mediation benchmark scenario. This schema covers ONLY contributor-authored fields. Server/operator fields are intentionally excluded: 'canary_string' (server-assigned, skip_serializing, never published) and 'is_held_out' (operator flag). Hard constraints: exactly 2 participants (additional stakeholders live in backstory/hidden_facts, never a 3rd participant); ASCII only (straight quotes/hyphens; transliterate accented names; no curly quotes, em-dashes, emoji); every participant has a non-empty archetype (a FREE-FORM string -- a single word like 'Skeptic' or a rich multi-sentence adversarial role-play persona; NOT a closed enum). CROSS-FIELD INVARIANTS JSON SCHEMA CANNOT ENFORCE STATICALLY (enforce in a validator/review): (a) each 'sample_turns[].speaker' MUST either exactly equal one of the two 'participants[].name' values OR be the literal 'Moderator' (the guide permits a neutral Moderator speaker; several shipped scenarios use it); (b) each resource's 'valuations' object MUST have keys that are EXACTLY the set of the two 'participants[].name' values (no missing party, no non-participant); (c) 'held_by' and any 'valuations' key must be one of the participant names; (d) a 'latent' resource's existence should be derivable from exactly one participant's 'hidden_facts' entry and the same content should appear in 'evaluation_criteria.expected_revelations'.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "name",
    "description",
    "conflict_type",
    "difficulty",
    "participants",
    "mediator_config",
    "expected_topics",
    "evaluation_criteria",
    "sample_turns"
  ],
  "properties": {
    "id": {
      "$ref": "#/$defs/snakeCaseId",
      "description": "URL-safe snake_case identifier; matches the scenario filename stem (e.g. 'cofounder_windup_ip')."
    },
    "name": {
      "$ref": "#/$defs/asciiStringNonEmpty",
      "description": "Short human-readable title."
    },
    "description": {
      "$ref": "#/$defs/asciiStringNonEmpty",
      "description": "2-4 neutral sentences describing the conflict and why mediation is needed."
    },
    "conflict_type": {
      "type": "string",
      "enum": [
        "interpersonal",
        "workplace",
        "family",
        "community",
        "financial",
        "legal",
        "commercial",
        "international",
        "civil_rights",
        "restorative"
      ],
      "description": "Category of conflict."
    },
    "difficulty": {
      "type": "string",
      "enum": [
        "novice",
        "intermediate",
        "expert"
      ],
      "description": "Difficulty based on emotional complexity and hidden dynamics. Per the guide, 'expert' scenarios should generally have weak or no ZOPA (impasse-prone); a comfortably solvable integrative scenario is usually 'intermediate'."
    },
    "participants": {
      "type": "array",
      "minItems": 2,
      "maxItems": 2,
      "description": "Exactly 2 conflicting parties. Other stakeholders belong in backstory/hidden_facts, never a 3rd participant.",
      "items": {
        "$ref": "#/$defs/participant"
      }
    },
    "mediator_config": {
      "$ref": "#/$defs/mediatorConfig"
    },
    "expected_topics": {
      "type": "array",
      "minItems": 4,
      "maxItems": 6,
      "description": "4-6 key topics that should be discussed during mediation.",
      "items": {
        "$ref": "#/$defs/asciiStringNonEmpty"
      }
    },
    "evaluation_criteria": {
      "$ref": "#/$defs/evaluationCriteria"
    },
    "sample_turns": {
      "type": "array",
      "minItems": 2,
      "maxItems": 5,
      "description": "Opening, alternating, in-character turns (typically 2-4; up to 5 is accepted, as at least one shipped scenario uses 5). Each 'speaker' MUST exactly match a participant 'name' OR be the literal 'Moderator' (cross-field invariant (a); not enforceable by enum here because participant names are dynamic).",
      "items": {
        "$ref": "#/$defs/sampleTurn"
      }
    },
    "resources": {
      "type": "array",
      "description": "OPTIONAL. Quantified negotiation resources for integrative (logrolling) scenarios. Omit entirely for purely relational conflicts. When present, supply >=2 resources (a single divisible resource is a fixed pie and cannot be integrative). Resources are objective design-time metadata and are SAFE to publish (no canary/held-out concern). RUNTIME NOTE: the current server-side 'BenchmarkScenario' struct has no 'resources' field and does not deny unknown fields, so a published 'resources' block deserializes to nothing today and is dropped on load -- it is captured for the FORMAT and for future scoring, and does not yet affect any run. SCORING INTENT: an agreed allocation over resources yields objective per-party utilities (utility = sum over resources of valuations[party][resource] * fraction_of_quantity_allocated_to_party) that can ground Pareto/NBS metrics and are INTENDED to become a future 'Resource/Value Efficiency' HAI Score component. Adding that 6th component would require re-weighting today's five categories (Cooperative 25 / Resolution 25 / Hidden Revelations 20 / Commitment Symmetry 15 / Mediator Quality 15); until then resources are inert and DO NOT affect the published HAI Score. INVARIANT: per-party utility is computed exclusively from 'valuations'; 'external_value_usd' is a display/realism anchor and must never enter any score.",
      "minItems": 2,
      "items": {
        "$ref": "#/$defs/resource"
      }
    }
  },
  "$defs": {
    "asciiStringNonEmpty": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[\\x20-\\x7E]+$",
      "description": "Non-empty printable-ASCII string (no curly quotes, em-dashes, emoji, or accented characters)."
    },
    "asciiStringMaybeEmpty": {
      "type": "string",
      "pattern": "^[\\x20-\\x7E]*$",
      "description": "Printable-ASCII string, possibly empty."
    },
    "snakeCaseId": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[a-z0-9]+(_[a-z0-9]+)*$",
      "description": "URL-safe snake_case identifier (lowercase ASCII letters/digits separated by single underscores)."
    },
    "unitEnum": {
      "type": "string",
      "enum": [
        "usd",
        "hours",
        "days",
        "count",
        "percent",
        "other"
      ],
      "description": "Measurement unit of the resource quantity. Use 'other' only with 'unit_label'. 'percent' is discouraged unless the description makes 'percent of what' explicit."
    },
    "participantBatna": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "utility",
        "description"
      ],
      "description": "Best Alternative To a Negotiated Agreement. 'utility' is normalized 0..1, directly comparable to resource-derived utilities; a party's BATNA above their best attainable allocation utility means there is no ZOPA.",
      "properties": {
        "utility": {
          "type": "number",
          "minimum": 0.0,
          "maximum": 1.0,
          "description": "Normalized walk-away utility, 0..1."
        },
        "description": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Human-readable description of the walk-away alternative."
        }
      }
    },
    "participant": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "backstory",
        "stated_goal",
        "hidden_facts",
        "archetype"
      ],
      "properties": {
        "name": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "ASCII first name. Must match exactly any 'sample_turns[].speaker' that refers to this party (a speaker may also be the literal 'Moderator', which is not a participant) and any 'valuations' key / 'held_by' that refers to this party."
        },
        "backstory": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "3-5 sentences: age, occupation, relevant history, and what brought them to this conflict."
        },
        "stated_goal": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "1-2 sentences: what this party openly claims to want (public position)."
        },
        "hidden_facts": {
          "type": "array",
          "minItems": 2,
          "description": ">=2 consequential facts this party knows but has not shared (real interests/leverage diverging from the stated goal). A latent resource should be derivable from exactly one of these entries.",
          "items": {
            "$ref": "#/$defs/asciiStringNonEmpty"
          }
        },
        "hidden_emotions": {
          "type": "array",
          "description": "Optional. 2-3 specific masked emotions (not just 'angry' but the specific grievance).",
          "items": {
            "$ref": "#/$defs/asciiStringNonEmpty"
          }
        },
        "archetype": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Required, non-empty, FREE-FORM communication/negotiation style archetype (NOT a closed enum; the runtime field is a plain Option<String>). It may be a single word (common values in the corpus include 'Advocate', 'Pragmatist', 'Skeptic', 'Caretaker', 'Competitor', 'Analyst', 'Idealist', 'Avoider', 'Accommodator') OR a rich multi-sentence adversarial role-play persona for hostile/uncensored scenarios (e.g. 'Belligerent Denier: stonewalls, mocks the process, minimizes harm, refuses accountability until confronted with concrete consequences'). This string is the role-play brief for the simulated participant."
        },
        "batna_config": {
          "$ref": "#/$defs/participantBatna",
          "description": "Optional BATNA for this party."
        }
      }
    },
    "mediatorConfig": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "description",
        "focus_areas",
        "web_search_enabled"
      ],
      "properties": {
        "description": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "2-3 sentences describing the ideal mediator, approach, and expertise."
        },
        "focus_areas": {
          "type": "array",
          "minItems": 3,
          "maxItems": 5,
          "description": "3-5 specific areas the mediator should focus on.",
          "items": {
            "$ref": "#/$defs/asciiStringNonEmpty"
          }
        },
        "web_search_enabled": {
          "type": "boolean",
          "description": "Whether the mediator has access to web search."
        }
      }
    },
    "evaluationCriteria": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "min_turns",
        "max_turns",
        "success_indicators"
      ],
      "properties": {
        "min_turns": {
          "type": "integer",
          "exclusiveMinimum": 0,
          "description": "Minimum conversation turns for meaningful engagement (typically 6-10)."
        },
        "max_turns": {
          "type": "integer",
          "exclusiveMinimum": 0,
          "description": "Maximum turns before the conversation should conclude (typically 20-30)."
        },
        "success_indicators": {
          "type": "array",
          "minItems": 3,
          "description": "3 or more observable conversation outcomes that indicate successful mediation. The guide sets no upper bound; most shipped scenarios use 5-7 (some use 4).",
          "items": {
            "$ref": "#/$defs/asciiStringNonEmpty"
          }
        },
        "expected_revelations": {
          "type": "array",
          "description": "Hidden facts a good mediator surfaces for a high score. Any 'latent' resource must be represented here.",
          "items": {
            "$ref": "#/$defs/asciiStringNonEmpty"
          }
        },
        "min_cooperative_percentage": {
          "type": [
            "number",
            "null"
          ],
          "minimum": 0.0,
          "maximum": 100.0,
          "description": "Optional. Minimum percentage of cooperative/constructive turns (typically 40-60; lower it for harder/weak-ZOPA scenarios)."
        }
      }
    },
    "sampleTurn": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "speaker",
        "message",
        "turn_number"
      ],
      "properties": {
        "speaker": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Must EXACTLY match a participant 'name' OR be the literal 'Moderator' (cross-field invariant (a) enforced by validator/review, not by this schema; participant names are dynamic so an enum cannot bind them)."
        },
        "message": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "In-character opening message (ASCII)."
        },
        "internal_thought": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Optional private inner thought for an AI participant."
        },
        "turn_number": {
          "type": "integer",
          "minimum": 1,
          "description": "1-indexed turn number."
        }
      }
    },
    "resource": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "name",
        "unit",
        "quantity",
        "divisibility",
        "valuations",
        "visibility"
      ],
      "description": "A quantified negotiation resource. 'valuations' keys MUST be exactly the two participant names (validator-enforced). Per-party utility from this resource = valuations[party] * fraction_of_quantity allocated to that party (linear, additive across resources). For 'indivisible' resources the fraction is binary (whole quantity or none).",
      "properties": {
        "id": {
          "$ref": "#/$defs/snakeCaseId",
          "description": "snake_case resource identifier, unique within the scenario."
        },
        "name": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Short human-readable resource name."
        },
        "unit": {
          "$ref": "#/$defs/unitEnum"
        },
        "unit_label": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Required when 'unit' == 'other'; human-readable unit name (ASCII)."
        },
        "quantity": {
          "type": "number",
          "exclusiveMinimum": 0,
          "description": "Total amount available to divide. For 'indivisible' resources this is the indivisible lot assigned whole (use quantity 1 for binary terms)."
        },
        "divisibility": {
          "type": "string",
          "enum": [
            "divisible",
            "indivisible",
            "shared"
          ],
          "description": "'divisible' = any fraction f in [0,1] to a party; 'indivisible' = whole quantity to one party or neither (never fractional); 'shared' = both parties may derive value simultaneously or it is a joint obligation (see 'shared_mode')."
        },
        "shared_mode": {
          "type": "string",
          "enum": [
            "both_benefit",
            "joint_obligation"
          ],
          "description": "Required when 'divisibility' == 'shared'. 'both_benefit' = non-rival good both can enjoy (add both valuations if kept shared); 'joint_obligation' = a cost/burden to assign."
        },
        "polarity": {
          "type": "string",
          "enum": [
            "asset",
            "liability"
          ],
          "default": "asset",
          "description": "Optional. 'asset' (default) = positive value. 'liability' = a cost/burden; valuations express the disutility a party suffers from being assigned it (the allocation question is who bears it)."
        },
        "external_value_usd": {
          "type": "number",
          "minimum": 0,
          "description": "Optional objective/market anchor for the WHOLE resource. Display/realism only; MUST NOT enter any utility/NBS/Pareto computation."
        },
        "valuations": {
          "type": "object",
          "minProperties": 2,
          "maxProperties": 2,
          "description": "Maps EACH participant name -> number 0..1 = that party's normalized value of the WHOLE resource (per-party weights should sum to 1.0 across all of a scenario's resources). Keys MUST be exactly the two participant names (validator-enforced; JSON Schema cannot bind to dynamic names). Different values across parties create the integrative trade space.",
          "additionalProperties": {
            "type": "number",
            "minimum": 0.0,
            "maximum": 1.0
          }
        },
        "valuation_basis": {
          "type": "string",
          "enum": [
            "normalized_share",
            "raw"
          ],
          "default": "normalized_share",
          "description": "Optional. 'normalized_share' (default) = per-party weights already normalized to sum to 1.0 across resources; 'raw' = scorer must renormalize."
        },
        "visibility": {
          "type": "string",
          "enum": [
            "known",
            "latent"
          ],
          "description": "'known' = on the table from turn 1; 'latent' = exists but must be surfaced during the conversation. A 'latent' resource must pair with a participant hidden_fact and appear in evaluation_criteria.expected_revelations. When 'latent', 'held_by' is required."
        },
        "held_by": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Optional for 'known' resources; REQUIRED for 'latent' resources. The participant name who knows/controls the resource (must be one of the two participant names)."
        },
        "description": {
          "$ref": "#/$defs/asciiStringNonEmpty",
          "description": "Optional short ASCII description."
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "unit": {
                "const": "other"
              }
            },
            "required": [
              "unit"
            ]
          },
          "then": {
            "required": [
              "unit_label"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "visibility": {
                "const": "latent"
              }
            },
            "required": [
              "visibility"
            ]
          },
          "then": {
            "required": [
              "held_by"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "divisibility": {
                "const": "shared"
              }
            },
            "required": [
              "divisibility"
            ]
          },
          "then": {
            "required": [
              "shared_mode"
            ]
          }
        }
      ]
    }
  }
}
