{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://fixragent.com/schemas/triage-response-v1.json",
  "title": "POST /api/triage \u2014 200 response body",
  "x-schema-id": "triage-response-v1",
  "x-version": "1.1.0",
  "x-compatibility": "ADDITIVE ONLY within v1. A field listed in `required` is never removed or renamed, its type is never changed, and an enum never loses or changes a value. New OPTIONAL fields may appear at any time \u2014 ignore fields you do not know. Anything that breaks those rules ships as triage-response-v2 at a new URL, and v1 keeps answering. The version is also sent on every response in the X-Triage-Schema header and in the body as schema_version. Only `core`, `customer` and the top-level identifiers are contract-stable; `extended`, `provenance`, `agreement`, `config_source` and `contract_validation` are diagnostic and may grow or reshape (they are typed here as objects only).",
  "x-changelog": [
    {
      "version": "1.0.0",
      "date": "2026-09-10",
      "what": "CONTRACT v1 core (20 fields) and the top-level envelope, as served since engine-v2-triage-door-1.0.0"
    },
    {
      "version": "1.1.0",
      "date": "2026-09-24",
      "what": "added `customer` (action, emergency, trade, sentence, language, find_a_pro_url), `data_use` (training_allowed, basis, request_opt_out, photo_bytes_stored) and `schema_version` \u2014 additive"
    }
  ],
  "type": "object",
  "required": [
    "diagnosis_id",
    "engine_version",
    "rubric_version",
    "config_id",
    "core",
    "customer",
    "schema_version",
    "warnings",
    "stored",
    "data_use",
    "channel",
    "channel_source",
    "language",
    "extended",
    "provenance",
    "agreement",
    "config_source",
    "contract_validation"
  ],
  "additionalProperties": true,
  "properties": {
    "diagnosis_id": {
      "type": "string",
      "format": "uuid",
      "description": "uuid4 minted by the server for this diagnosis."
    },
    "schema_version": {
      "type": "string",
      "pattern": "^triage-response-v1@1\\.[0-9]+\\.[0-9]+$",
      "description": "This schema id and version, e.g. \"triage-response-v1@1.1.0\"; the same value as the X-Triage-Schema header. Any 1.x.y is compatible with this schema."
    },
    "engine_version": {
      "type": "string",
      "description": "Which engine build produced this."
    },
    "rubric_version": {
      "type": "string",
      "description": "Which rubric assigned severity and tier."
    },
    "config_id": {
      "type": "string",
      "description": "The engine config snapshot. An id beginning \"fallback-\" means the measured config could not be loaded."
    },
    "core": {
      "$ref": "#/$defs/core"
    },
    "customer": {
      "$ref": "#/$defs/customer"
    },
    "warnings": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Anything the caller should not miss. Empty on a clean run."
    },
    "stored": {
      "type": "object",
      "description": "Whether and where this diagnosis was stored.",
      "additionalProperties": true
    },
    "callback_url": {
      "type": "string",
      "description": "The /api/outcome path for this diagnosis_id. Absent on a sandbox key."
    },
    "channel": {
      "type": "string",
      "description": "The door this read came through."
    },
    "language": {
      "type": "object",
      "description": "Which language the prose fields were asked for and served in.",
      "additionalProperties": true
    },
    "extended": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": true,
      "description": "Everything else the engine produced. NULL unless the request asks for it (?extended=1 or body extended:true). Diagnostic, not contract-stable."
    },
    "provenance": {
      "type": "object",
      "description": "How this answer was produced. Diagnostic, not contract-stable.",
      "additionalProperties": true
    },
    "agreement": {
      "type": "object",
      "description": "Agreement between independent reads of the same photo. Diagnostic.",
      "additionalProperties": true
    },
    "config_source": {
      "type": "object",
      "description": "Where the config came from. Diagnostic.",
      "additionalProperties": true
    },
    "contract_validation": {
      "type": "object",
      "description": "What the server checked and repaired on the core. Diagnostic.",
      "additionalProperties": true
    },
    "data_use": {
      "$ref": "#/$defs/data_use"
    },
    "channel_source": {
      "type": "string",
      "description": "Which rule decided `channel`."
    },
    "callback": {
      "type": "object",
      "additionalProperties": true,
      "description": "How to report the outcome (POST /api/outcome). Absent on a sandbox key.",
      "required": [
        "method",
        "url"
      ],
      "properties": {
        "method": {
          "type": "string"
        },
        "url": {
          "type": "string"
        }
      }
    }
  },
  "$defs": {
    "core": {
      "type": "object",
      "description": "CONTRACT v1 CORE \u2014 exactly 20 fields, in this order. 15 are CONTRACT-v1.json fields under their contract names; 5 (tier, criterion_1_fired, tier_forced_by, visible_wiring, water_near_electrical) are RUBRIC v1.1-2026-09-11 additions that CONTRACT-v1 predates. Everything the engine produced beyond these rides in `extended`; nothing is dropped.",
      "required": [
        "is_building_asset",
        "asset_type",
        "asset_category",
        "photo_subject",
        "report_photo_agreement",
        "fault_detected",
        "fault_visible_in_photo",
        "fault_summary",
        "fault_classes",
        "severity",
        "tier",
        "criterion_1_fired",
        "tier_forced_by",
        "visible_wiring",
        "water_near_electrical",
        "safety_hazard",
        "hazard_detail",
        "trade_required",
        "confidence",
        "resident_explanation"
      ],
      "additionalProperties": true,
      "properties": {
        "is_building_asset": {
          "description": "False when the photo is a person, pet, vehicle, food or document. False forces severity P4 before anything else is evaluated.",
          "type": "boolean"
        },
        "asset_type": {
          "description": "The specific asset, named precisely (\"Gas water heater\", not \"appliance\"). Null only when is_building_asset is false.",
          "type": [
            "string",
            "null"
          ]
        },
        "asset_category": {
          "description": "HVAC | PLUMBING | ELECTRICAL | APPLIANCE | STRUCTURAL | OTHER | NOT_AN_ASSET. An engine value outside this list is repaired to OTHER by the server.",
          "type": "string",
          "enum": [
            "HVAC",
            "PLUMBING",
            "ELECTRICAL",
            "APPLIANCE",
            "STRUCTURAL",
            "OTHER",
            "NOT_AN_ASSET"
          ]
        },
        "photo_subject": {
          "description": "What is actually in frame, named from the pixels alone and independent of anything the reporter said. This is the field that catches a fabricated diagnosis of an asset that is not in the photo.",
          "type": "string"
        },
        "report_photo_agreement": {
          "description": "\"true\" | \"false\" | \"uncertain\" \u2014 does the reported problem describe what the photo shows. Null ONLY when no problem_text accompanied the photo.",
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "true",
            "false",
            "uncertain",
            null
          ]
        },
        "fault_detected": {
          "description": "The scored verdict. Rides the photo: a report-only symptom is a hazard input, never a fault.",
          "type": "boolean"
        },
        "fault_visible_in_photo": {
          "description": "Whether the named fault is actually visible. Null when the engine did not answer.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "fault_summary": {
          "description": "One paragraph for the person dispatching. Empty string when no fault.",
          "type": "string"
        },
        "fault_classes": {
          "description": "Taxonomy-v0 class ids, primary first. Empty array whenever fault_detected is false. Ids not in the taxonomy are dropped by the server rather than passed through.",
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "panel-fault",
              "burnt-charred-wiring",
              "damaged-outlet-switch",
              "deteriorated-legacy-wiring",
              "exposed-improper-wiring",
              "electrical-fixture-fault",
              "other-electrical",
              "gas-leak-piping-fault",
              "water-heater-gas-control-fault",
              "water-heater-tank-leak-corrosion",
              "tpr-valve-discharge-fault",
              "active-plumbing-leak",
              "frozen-pipe",
              "sewage-drain-backup",
              "toilet-leak-running",
              "fixture-wear-fault",
              "other-plumbing",
              "combustion-venting-co-fault",
              "heating-cooling-outage",
              "condensate-leak-blockage",
              "refrigerant-circuit-fault",
              "condenser-coil-degradation",
              "ductwork-fault",
              "radon-mitigation-failure",
              "other-hvac",
              "gas-appliance-fault",
              "appliance-electrical-burn",
              "battery-swelling-hazard",
              "appliance-water-leak",
              "refrigerator-cooling-fault",
              "refrigerator-water-system-fault",
              "laundry-mechanical-fault",
              "dryer-vent-lint-fault",
              "cooking-appliance-fault",
              "small-appliance-degradation",
              "other-appliance",
              "structural-sag-collapse",
              "water-damaged-structure",
              "foundation-crack-fault",
              "active-water-intrusion",
              "mold-growth",
              "interior-surface-damage",
              "door-window-hardware-fault",
              "other-structural",
              "roof-structure-rot",
              "roof-active-leak",
              "roof-membrane-degradation",
              "siding-envelope-damage",
              "other-roofing-exterior",
              "equipment-tool-fault",
              "site-grounds-fault",
              "soiling-maintenance-buildup",
              "cosmetic-surface-wear",
              "other-other"
            ]
          }
        },
        "severity": {
          "description": "P1 | P2 | P3 | P4. ASSIGNED BY THE SERVER from the rubric axes. Never taken from the model \u2014 whatever the model proposed is kept in provenance.model_proposed_severity instead.",
          "type": "string",
          "enum": [
            "P1",
            "P2",
            "P3",
            "P4"
          ]
        },
        "tier": {
          "description": "EMERGENCY | TODAY | THIS WEEK | WHENEVER. A pure function of severity, assigned in code. The only urgency wording a Triage Profile is allowed to use.",
          "type": "string",
          "enum": [
            "EMERGENCY",
            "TODAY",
            "THIS WEEK",
            "WHENEVER"
          ]
        },
        "criterion_1_fired": {
          "description": "True when the RUBRIC v1.1 EMERGENCY floor forced P1 \u2014 visible wiring, or water near anything electrical. This is the one legal way a diagnosis reaches P1 with fault_detected false.",
          "type": "boolean"
        },
        "tier_forced_by": {
          "description": "Which rule overrode the decision table, when one did: \"criterion-1:visible_wiring\", \"criterion-1:water_near_electrical\", \"not-a-building-asset\", or \"no-fault-detected-clamp\".",
          "type": [
            "string",
            "null"
          ]
        },
        "visible_wiring": {
          "description": "Photo-borne fact: a current-carrying conductor, splice or terminal a person could touch without a tool. Low-voltage wiring is excluded by definition. Uncertain reads as false, on purpose.",
          "type": "boolean"
        },
        "water_near_electrical": {
          "description": "Photo-borne fact: water, wetting or wetting-corrosion in contact with, above, or beneath an electrical device. Uncertain reads as false, on purpose.",
          "type": "boolean"
        },
        "safety_hazard": {
          "description": "A hazard EXISTS. Additive \u2014 it never replaces the explanation, and it is true whenever the hazard axis is at least H1. Every P1 carries a hazard; not every hazard is a P1.",
          "type": "boolean"
        },
        "hazard_detail": {
          "description": "The named hazard. Non-empty exactly when safety_hazard is true, null exactly when it is false.",
          "type": [
            "string",
            "null"
          ]
        },
        "trade_required": {
          "description": "The trade to dispatch. Null when there is no fault.",
          "type": [
            "string",
            "null"
          ]
        },
        "confidence": {
          "description": "Calibrated 0-1 probability that the fault_detected verdict is correct. Out-of-range values are REJECTED to null, never rescaled on a guess.",
          "type": [
            "number",
            "null"
          ],
          "minimum": 0,
          "maximum": 1
        },
        "resident_explanation": {
          "description": "Plain language, for the resident. Always filled, never \"contact maintenance\".",
          "type": "string"
        }
      }
    },
    "customer": {
      "type": "object",
      "additionalProperties": true,
      "description": "What a partner shows its own customer, under its own brand. Every value is derived in code from `core` (never copied from model text), so none of these fields ever carries a brand name or an empty sentence.",
      "required": [
        "action",
        "emergency",
        "trade",
        "sentence",
        "language",
        "find_a_pro_url"
      ],
      "properties": {
        "action": {
          "type": "string",
          "enum": [
            "call_now",
            "book",
            "monitor",
            "no_fault"
          ],
          "description": "call_now = severity P1, a gas/CO fault class, a criterion-1 electrical fact, or a hazard the rubric scores H2 (life-safety now; a reported gas smell is H2); book = P2, or any other named hazard; monitor = P3, or a P4 with a named fault; no_fault = no fault, or not a building asset."
        },
        "emergency": {
          "type": "boolean",
          "description": "True exactly when action is call_now."
        },
        "trade": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "plumbing",
            "electrical",
            "hvac",
            "appliance",
            "roofing",
            "structural",
            "general",
            null
          ],
          "description": "Which trade this belongs to. Null only when the photo is not a building asset."
        },
        "sentence": {
          "type": "string",
          "minLength": 1,
          "description": "One or two plain sentences for the end customer, in `language`. No brand, no model hedging. A gas/CO fault class says leave the building; a criterion-1 electrical fact says keep away and switch off the power; any other named hazard says there is a safety concern and, IF you smell gas, leave the building \u2014 never \"not urgent\"."
        },
        "language": {
          "type": "string",
          "enum": [
            "en",
            "es"
          ],
          "description": "Language of `sentence`."
        },
        "find_a_pro_url": {
          "type": [
            "string",
            "null"
          ],
          "description": "OPTIONAL to use: a plain Google Maps \"<trade> near me\" search (no partner id, no affiliate tag) on call_now and book; null otherwise, and always null on a gas/CO emergency (the gas company comes first)."
        }
      }
    },
    "data_use": {
      "type": "object",
      "additionalProperties": true,
      "description": "Whether this call may ever be used to train or tune a model. Off by default; on only for a key whose holder authorised it in writing. A request can opt out (body do_not_train:true or header x-triage-do-not-train: 1); nothing in a request can opt in. No pipeline reads this flag yet, because nothing trains on these rows today; it is the one field any future export must honour.",
      "required": [
        "training_allowed",
        "basis",
        "request_opt_out",
        "photo_bytes_stored"
      ],
      "properties": {
        "training_allowed": {
          "type": "boolean"
        },
        "basis": {
          "type": "string"
        },
        "request_opt_out": {
          "type": "boolean"
        },
        "photo_bytes_stored": {
          "type": "boolean",
          "description": "Always false on this endpoint: only the SHA-256 of the EXIF-stripped photo is kept."
        }
      }
    }
  }
}
