{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/frase-io/json-schema/frase-io-fact-schema.json",
  "title": "Fact",
  "description": "One claim in a brief's fact ledger. Colour in the UI is provenance x verification; the API returns both so a client can reproduce it.",
  "x-generated": "2026-11-10",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/frase-io-briefs-api-openapi.yml#/components/schemas/Fact",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "claim": {
      "type": "string"
    },
    "figure": {
      "type": [
        "object",
        "null"
      ],
      "description": "The typed number the claim states, when it states one.",
      "properties": {
        "value": {
          "type": "number"
        },
        "unit": {
          "type": "string"
        },
        "asOf": {
          "type": "string"
        }
      }
    },
    "provenance": {
      "type": "string",
      "enum": [
        "research_grounded",
        "research_asserted",
        "customer",
        "imported",
        "generated"
      ]
    },
    "sources": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/FactSource"
      }
    },
    "removed_sources": {
      "type": "array",
      "description": "Decision receipts for sources removed from this claim. They are not citable sources; use their URL to address a later restoring decision.",
      "items": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "decided_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "verification": {
      "type": "string",
      "enum": [
        "verified",
        "undetermined",
        "unverified",
        "refuted",
        "customer_attested"
      ]
    },
    "approval": {
      "type": "string",
      "enum": [
        "pending",
        "approved",
        "rejected"
      ],
      "description": "Minted facts default to `approved` — the flag is what lets a claim into generation, and a ledger that starts everything at `pending` is a queue nobody works. A rejection is permanent: re-minting the same claim never resets it. Claims the writer asserted in its own output (`provenance: generated`) are the one class held at `pending`."
    },
    "section_binding": {
      "type": [
        "string",
        "null"
      ],
      "description": "Normalised heading this claim belongs to, when bound."
    },
    "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "Echo this as `expected_updated_at` on the approval PATCH."
    }
  },
  "$defs": {
    "FactSource": {
      "type": "object",
      "description": "Where a claim came from, and what we know about the page behind it. `url` and `domain` are absent when the research named a publisher without capturing a link — the common case. A named source is attribution, not grounding: it does not raise `provenance` or `verification`.\n\nADDED 2026-09-14: `state`, `stateReason` and `finding`. Additive and optional — absent on every source minted before that date, and no existing field changed.",
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Absent when the source was named but not linked."
        },
        "decision_url": {
          "type": "string",
          "format": "uri",
          "description": "The durable source-decision key. Usually identical to `url`; after a replacement it remains the original URL. Send this value as `source_url` when changing the decision again."
        },
        "title": {
          "type": "string",
          "description": "Publisher or page title."
        },
        "domain": {
          "type": "string",
          "description": "Derived from `url`; absent whenever it is."
        },
        "tier": {
          "type": "string",
          "description": "Authority tier from the citations classifier, when known."
        },
        "verbatimQuote": {
          "type": "string",
          "description": "The excerpt from the source carrying the claim."
        },
        "retrievedAt": {
          "type": "string",
          "format": "date-time"
        },
        "state": {
          "type": "string",
          "enum": [
            "ai_verified",
            "human_verified",
            "unverified",
            "needs_human_check",
            "discarded"
          ],
          "description": "What we know about the page behind `url`. `ai_verified` — we opened it and found the claim, and `verbatimQuote` is copied from it. `human_verified` — a person vouched for it, and re-running research will not change it. `unverified` — nothing established the claim either way; see `stateReason`. `needs_human_check` — the site refused our request, usually bot protection rather than a missing page. `discarded` — the page is gone, so the claim is kept and the link is not usable. Absent on sources minted before 2026-09-14."
        },
        "stateReason": {
          "type": "string",
          "description": "Why `state` is what it is. The one value worth branching on is `unsupported`: the page was READ and the claim was NOT on it, which is a different situation from not having been checked — and the two are indistinguishable from `state` alone, because both are `unverified`. Other values describe the fetch (`http_gone`, `blocked`, `not_parsed`, `unreachable`, `reachable`) and are informational."
        },
        "finding": {
          "type": "string",
          "description": "The checker's own sentence when it read the page and did not find the claim. Model-authored prose meant for a person to read, present only with `stateReason: \"unsupported\"`. Never parse it — branch on `stateReason` and show this."
        }
      }
    }
  }
}