{
  "openapi": "3.1.0",
  "info": {
    "title": "Heartwood local API",
    "description": "",
    "license": {
      "name": ""
    },
    "version": "v1"
  },
  "paths": {
    "/v1/assertions": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "List assertions, paginated",
        "description": "The claims recorded against one persona, cursor-paginated. `persona`\nis **required** — this route is scoped to a persona, not to a person,\nso start from `GET /v1/persons/{id}` if all you have is a person id.\n\n`kind=`, `state=` and `source=` narrow the set before it is paginated,\nso `total` counts the matches. An unrecognised query parameter is a\n`400` rather than being ignored.",
        "operationId": "list_assertions",
        "parameters": [
          {
            "name": "persona",
            "in": "query",
            "description": "**Required.** Assertions are listed per persona, so a caller\nholding a person id resolves it first — `GET /v1/persons/{id}`\nreturns that person's persona ids.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "Keep only assertions whose `kind` equals this string exactly (e.g.\n`\"fact\"`) — the same vocabulary a returned assertion's `kind` uses.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "Keep only assertions whose epistemic status equals this string\nexactly (e.g. `\"confirmed\"`). The parameter is spelled `state`; the\nfield it filters is spelled `status` in the response body. Same\nvocabulary, different name.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Keep only assertions whose `source` equals this source id.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "How to order the results. `\"created\"` — by the assertion's\n`created_at_ms` — is the only value accepted today; anything else\nis a 400. Absent ⇒ ordering by id.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`asc` (default, oldest first) or `desc` (newest first). A cursor is\nonly valid against the order it was minted under.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_AssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/capture": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Capture a new assertion with its evidence",
        "operationId": "capture_assertion",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CaptureRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/transcription-claim": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Claim a transcription of a source blob",
        "operationId": "post_transcription_claim",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClaimTranscriptionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/unsourced": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "List assertions awaiting human evaluation, paginated",
        "operationId": "list_unsourced_assertions",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_UnsourcedAssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/unsourced/tally": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "Count how unsourced-surface claims split into sourced and needs-source",
        "description": "A client cannot get this by reading the listing's page `total`: that\nfigure counts both halves, so a surface labelling it \"unsourced\" says\nthe whole tree is unsourced, including the claims it renders as\nsourced on the very same screen. `total` here is the sum of the two\nhalves, never an independent count.",
        "operationId": "tally_unsourced_assertions",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourcingTallyDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/{id}": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "Get an assertion by id",
        "operationId": "get_assertion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssertionDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/{id}/attach-evidence": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Attach evidence to an existing assertion",
        "operationId": "attach_evidence",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AttachEvidenceRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/{id}/confirm": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Confirm a pending assertion",
        "operationId": "confirm_assertion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/{id}/mark-fabricated": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Mark an assertion as fabricated",
        "operationId": "mark_fabricated",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/{id}/refute": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Refute an assertion",
        "operationId": "refute_assertion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assertions/{id}/supersede": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Supersede an assertion with a replacement",
        "operationId": "post_assertion_supersede",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SupersedeAssertionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/assignments/{id}": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "Get a recorded assignment by id",
        "operationId": "get_assignment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignmentDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/associations": {
      "post": {
        "tags": [
          "associations"
        ],
        "summary": "Create an association between persons",
        "operationId": "post_association",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordAssociationRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssociationIdDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/audit": {
      "get": {
        "tags": [
          "audit"
        ],
        "summary": "Get the audit trail",
        "operationId": "get_audit",
        "parameters": [
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_AuditEntryDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/backup/config": {
      "get": {
        "tags": [
          "backup"
        ],
        "summary": "Get the backup configuration",
        "operationId": "get_backup_config",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackupConfigDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "put": {
        "tags": [
          "backup"
        ],
        "summary": "Set the backup configuration",
        "operationId": "put_backup_config",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PutBackupConfigRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackupConfigDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/backup/manifests": {
      "get": {
        "tags": [
          "backup"
        ],
        "summary": "List the backups under a backup root",
        "description": "Lists the backups found under a backup root — including a root this\nmachine has never had configured, which is the case that matters when\nrestoring onto a replacement computer. Requires the human token.\n\nThe service walks the root and parses each manifest itself, refusing\nany whose format version it does not understand rather than guessing.\nA root that was read and holds no recognizable backups returns an\nempty list. A root that could **not** be read — absent, not a folder,\nor not a full path — is `422`, never an empty list: an unmounted drive\nis this flow's likeliest failure, and \"no backups found\" is the one\nanswer that would make a user stop looking for data that is still\nthere. Nothing in the response carries a filesystem path back out.",
        "operationId": "get_backup_manifests",
        "parameters": [
          {
            "name": "backup_root",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackupManifestsDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/backup/status": {
      "get": {
        "tags": [
          "backup"
        ],
        "summary": "Get the backup status",
        "operationId": "get_backup_status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackupStatusDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/blobs": {
      "post": {
        "tags": [
          "blobs"
        ],
        "summary": "Upload a blob",
        "description": "Stores raw bytes and returns their content address. Send the bytes as\nthe request body with `Content-Type: application/octet-stream`; the\naddress that comes back is what every other route means by a blob\nreference (bare hex, 64 characters).\n\nThe body is streamed rather than buffered, so upload size is bounded\nby the ceiling below rather than by memory. A body that exceeds the\nceiling is refused with `413` while it is still arriving, and the\npartial upload is discarded — a rejected upload leaves nothing\nbehind.",
        "operationId": "post_blob",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "raw bytes, streamed",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 0
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlobRefDto"
                }
              }
            }
          },
          "413": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/blobs/{address}": {
      "get": {
        "tags": [
          "blobs"
        ],
        "summary": "Download a blob by content address",
        "operationId": "get_blob",
        "parameters": [
          {
            "name": "address",
            "in": "path",
            "description": "sha256:<hex> content address",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "application/octet-stream blob bytes"
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/capabilities": {
      "get": {
        "tags": [
          "capabilities"
        ],
        "summary": "Get the daemon's advertised capabilities",
        "operationId": "get_capabilities",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CapabilitiesDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/citation-templates": {
      "get": {
        "tags": [
          "citations"
        ],
        "summary": "List available citation templates",
        "operationId": "get_citation_templates",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitationTemplatesListDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/citations": {
      "post": {
        "tags": [
          "citations"
        ],
        "summary": "Create a general-purpose citation from typed elements",
        "operationId": "post_citation",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCitationRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateCitationResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/citations/render": {
      "post": {
        "tags": [
          "citations"
        ],
        "summary": "Render a citation to formatted text",
        "operationId": "post_citation_render",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CitationRenderRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitationRenderResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/citations/{id}/elements": {
      "put": {
        "tags": [
          "citations"
        ],
        "summary": "Set a citation's structured elements",
        "operationId": "put_citation_elements",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetCitationElementsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationElementsResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationElementsResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/citations/{id}/source-type": {
      "put": {
        "tags": [
          "citations"
        ],
        "summary": "Record what kind of record a source is",
        "operationId": "put_citation_source_type",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetCitationSourceTypeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationSourceTypeResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationSourceTypeResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/conflicts": {
      "get": {
        "tags": [
          "conflicts"
        ],
        "summary": "List detected conflicts",
        "description": "Each row names the persona or person it belongs to and carries the\nclaims that disagree, each already joined to the source that made it\n— so a caller can describe the dispute without fetching anything\nelse. Filter with `persona` or `person` to get only one subject's\nconflicts; pass neither for the whole tree.",
        "operationId": "list_conflicts",
        "parameters": [
          {
            "name": "persona",
            "in": "query",
            "description": "Only conflicts recorded against this persona.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "person",
            "in": "query",
            "description": "Only conflicts about this person's identity, plus every conflict\non a persona this person concludes over.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ConflictDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/conflicts/{id}/resolution": {
      "post": {
        "tags": [
          "conflicts"
        ],
        "summary": "Resolve a conflict with a proof argument",
        "description": "Returns the updated conflict, now closed. A conflict is settled once\nand only once: resolving one that is already settled is a `409`, and\nnaming a conflict or an argument that does not exist is a `404`.",
        "operationId": "post_conflict_resolution",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResolveConflictRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConflictDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/diagnostics": {
      "get": {
        "tags": [
          "diagnostics"
        ],
        "summary": "Get diagnostics summary",
        "operationId": "get_diagnostics",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiagnosticsDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/diagnostics/bundle": {
      "get": {
        "tags": [
          "diagnostics"
        ],
        "summary": "Download a diagnostics bundle",
        "operationId": "get_diagnostics_bundle",
        "responses": {
          "200": {
            "description": "text/plain redacted diagnostics bundle"
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/events": {
      "post": {
        "tags": [
          "events"
        ],
        "summary": "Create a life event",
        "operationId": "post_event",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordLifeEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordedLifeEventDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/events/stream": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Subscribe to the live change-event stream (SSE)",
        "description": "A `text/event-stream` of change notices, so a client can refetch what\nmoved instead of polling. The feed is best-effort and has no replay: a\nclient that connects late has missed the earlier events and catches up\nby re-reading state, not by rewinding the stream.\n\nThe two mounts of this route behave differently, deliberately:\n\n* `/v1/trees/{tree_id}/events/stream` stays pinned to that one tree\n  for the life of the connection.\n* `/v1/events/stream` follows whichever tree is currently active. When\n  the active tree changes it re-subscribes to the new tree and emits a\n  `tree.active_changed` event, which is a client's cue to refetch\n  once.",
        "operationId": "sse_stream",
        "responses": {
          "200": {
            "description": "text/event-stream of change notices"
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/events/{id}": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Get a life event by id",
        "operationId": "get_event",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LifeEventDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/events/{id}/corrections": {
      "post": {
        "tags": [
          "events"
        ],
        "summary": "Correct a life event's details",
        "operationId": "post_event_correction",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CorrectEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CorrectionResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CorrectionResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/events/{id}/participants": {
      "post": {
        "tags": [
          "events"
        ],
        "summary": "Add a participant to a life event",
        "operationId": "post_event_participant",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddParticipantRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipationIdDto"
                }
              }
            }
          },
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipationIdDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/fan/query": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Query the fan-network relationship view by parameters",
        "operationId": "get_fan_network_query",
        "parameters": [
          {
            "name": "persona_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "person_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "window_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "window_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "edge_types",
            "in": "query",
            "description": "Comma-separated `shared_event` / `association` / `place_co_occurrence`\n(default: all three).",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FanNetworkDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/feedback": {
      "post": {
        "tags": [
          "diagnostics"
        ],
        "summary": "Send feedback to the Heartwood team",
        "description": "Sends user-typed feedback to the Heartwood team. Requires the human\ntoken: this is the one route that sends text off the machine, so an\nagent credential can never call it.\n\nThe request body carries only what the user typed. The app version and\nplatform are filled in by the service itself and echoed back in the\nresponse, so a client cannot misreport which build a message came from\n— and can show the user the exact values before sending.\n\nThe outcome is never flattened into a generic success. `202` means\ndelivered; `429` rate limited; `503` the service is not accepting\nsubmissions; `502` it could not be reached; `422` the message was\nrefused before it left the machine. No response reports success for a\nmessage that was not delivered.\n\nTier: `preview` — a first-party surface for this app's own window, not\nsomething a third party should build against, but it may still change\nshape between minor versions.",
        "operationId": "post_feedback",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitFeedbackRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "delivered to the feedback service",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackAcceptedDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/gedcom/export": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Export the tree to GEDCOM (family file) format",
        "operationId": "post_gedcom_export",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GedcomExportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GedcomExportResultDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/gedcom/loss-reports": {
      "get": {
        "tags": [
          "gedcom"
        ],
        "summary": "Get a GEDCOM import's loss report",
        "operationId": "get_gedcom_loss_report",
        "parameters": [
          {
            "name": "report_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LossReportDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/gedcom/validate": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Validate a GEDCOM (family file) without importing",
        "operationId": "post_gedcom_validate",
        "parameters": [
          {
            "name": "version_hint",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "raw GEDCOM bytes",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 0
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LossReportDto"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/imports": {
      "get": {
        "tags": [
          "gedcom"
        ],
        "summary": "List import runs",
        "operationId": "get_imports",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ImportRecordDto"
                  }
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/imports/gedcom": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Import a GEDCOM (family file) into the tree",
        "operationId": "post_import_gedcom",
        "parameters": [
          {
            "name": "filename",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "allow_duplicate",
            "in": "query",
            "description": "Default `false`. Uploading bytes identical to a previous import\nrefuses synchronously with `409 duplicate-import`, before any job\nis spawned. Set this to import the same file a second time on\npurpose.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "raw GEDCOM bytes",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 0
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptedJobDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/imports/{import_key}/undo": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Undo a GEDCOM import",
        "operationId": "post_import_undo",
        "parameters": [
          {
            "name": "import_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UndoImportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptedJobDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/integrations/claude-desktop/connect": {
      "post": {
        "tags": [
          "integrations"
        ],
        "summary": "Connect Claude Desktop to this instance",
        "operationId": "connect_claude_desktop",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectClaudeDesktopResultDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/integrations/claude-desktop/connect/preview": {
      "post": {
        "tags": [
          "integrations"
        ],
        "summary": "Preview connecting Claude Desktop, without writing anything",
        "operationId": "preview_connect_claude_desktop",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HarnessConnectPreviewDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/integrations/harnesses": {
      "get": {
        "tags": [
          "integrations"
        ],
        "summary": "List known harnesses and which support one-click connect",
        "operationId": "list_harnesses",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HarnessListResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/jobs": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "List background jobs",
        "operationId": "list_jobs",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_JobDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "Get a background job's status",
        "operationId": "get_job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/jobs/{id}/cancel": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Cancel a background job",
        "operationId": "cancel_job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "202": {
            "description": "cancel requested"
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/labels": {
      "get": {
        "tags": [
          "labels"
        ],
        "summary": "Resolve entity ids to display labels",
        "description": "Turns a batch of entity ids into the names a human reads. Reads that\nreturn ids only — `GET /v1/audit` above all — pair with this route so\na caller can render a readable timeline in one extra call instead of\none call per row.\n\nIds are globally unique across every entity kind, so no kind hint is\nneeded or accepted. An id that resolves to nothing is **omitted from\nthe response** rather than reported as an error: a partly-resolving\nbatch is the normal case (an entity may have been redacted or\nremoved), and the response tells you what it found, in any order.\n\nEvery label returned here is one the same caller could already read\nfrom `GET /v1/persons/{id}` and its siblings; this route is a\nconvenience, not a new grant.",
        "operationId": "get_labels",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "description": "Comma-separated entity ids.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EntityLabelDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/media/unresolved": {
      "get": {
        "tags": [
          "media"
        ],
        "summary": "List media this machine has not read in, by file name",
        "description": "No folder is reported and none is accepted: a caller supplies bytes by\nuploading them to `POST /v1/blobs` and naming the returned address to\n`POST /v1/media/{id}/resolve`. Returns every row in one response.",
        "operationId": "get_unresolved_media",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/UnresolvedMediaDto"
                  }
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/media/{id}/resolve": {
      "post": {
        "tags": [
          "media"
        ],
        "summary": "Resolve a media object from an already-uploaded blob",
        "description": "The blob becomes this tree's own copy, so whatever the caller read it\nfrom may afterwards be moved, renamed or deleted with no effect here.\nAnswers `200` with `status: \"already_resolved\"` when the file already\nread from these exact bytes — a no-op rather than a refusal. Naming a\n*different* blob for a file that already has one is a correction, not\na no-op: it lands, and `status` is `resolved`. That is what makes\nattaching the wrong file recoverable, since `media.resolved.v1` has no\nundo route.",
        "operationId": "post_media_resolve",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResolveMediaRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaResolveResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/notes": {
      "get": {
        "tags": [
          "notes"
        ],
        "summary": "List notes",
        "operationId": "get_notes",
        "parameters": [
          {
            "name": "target",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NoteDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "notes"
        ],
        "summary": "Create a note",
        "operationId": "post_note",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddNoteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteIdDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/pending": {
      "get": {
        "tags": [
          "pending"
        ],
        "summary": "List assertions in the pending lane",
        "description": "The review queue: assertions an agent has proposed that no human has\naccepted or rejected yet. Cursor-paginated like every other collection\nroute.\n\n`source=` and `proposer=` narrow the set; `sort=created` with `order=`\ncontrols ordering; `group_by=source|proposer` switches the response\nfrom a flat page to buckets — see the response schema for how the two\nshapes differ.\n\nConfirming or rejecting an item removes it from this queue: use\n`POST /v1/assertions/{id}/confirm`, `.../refute`, or\n`POST /v1/pending/bulk-confirm` for a whole batch.",
        "operationId": "get_pending",
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "description": "Keep only pending items whose `source` equals this source id.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "proposer",
            "in": "query",
            "description": "Keep only pending items whose `proposer` equals this string — the\nmodel identifier the creating agent reported for itself.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "How to order the results. `\"created\"` is the only value accepted\ntoday.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`asc` (default, oldest first) or `desc` (newest first). A cursor is\nonly valid against the order it was minted under.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "group_by",
            "in": "query",
            "description": "Return the (already filtered and sorted) result set bucketed by\n`source` or by `proposer` instead of as a flat page. See this\nroute's own description for how that changes the response shape.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PendingPageDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/pending/bulk-confirm": {
      "post": {
        "tags": [
          "pending"
        ],
        "summary": "Bulk-confirm pending assertions",
        "operationId": "bulk_confirm_pending",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkConfirmRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkConfirmResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/pending/{id}/defer": {
      "post": {
        "tags": [
          "pending"
        ],
        "summary": "Defer a pending assertion",
        "operationId": "defer_pending",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/pending/{id}/undefer": {
      "post": {
        "tags": [
          "pending"
        ],
        "summary": "Undefer a previously-deferred pending assertion",
        "operationId": "undefer_pending",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/personas": {
      "post": {
        "tags": [
          "personas"
        ],
        "summary": "Create a persona",
        "operationId": "post_persona",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePersonaRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/personas/{id}": {
      "get": {
        "tags": [
          "personas"
        ],
        "summary": "Get a persona by id",
        "operationId": "get_persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonaDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/personas/{id}/name-pieces": {
      "put": {
        "tags": [
          "personas"
        ],
        "summary": "Set a persona's structured name pieces",
        "operationId": "put_name_pieces",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetNamePiecesRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "List persons, paginated",
        "description": "Every person in the tree, cursor-paginated. `q=` searches names,\n`sort=`/`order=` control ordering, and `limit=`/`cursor=` page through\nthe result — see each parameter for the details.\n\nA query parameter this route does not recognise is a `400`, never\nsilently ignored, so a typo in a filter name fails loudly instead of\nreturning an unfiltered page that looks right.",
        "operationId": "list_persons",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Free-text person-name search. Matches a name prefix first and falls\nback to a substring match when no prefix hits. Results come back in\nrelevance order, so a `sort`/`order` given alongside `q` is accepted\nbut ignored rather than rejected. Absent ⇒ no name filter.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "How to order the results:\n\n* `name` — the person's display name.\n* `surname` — the display name's last whitespace-separated token,\n  then the full name.\n* `birth` — earliest recorded birth year; people with no recorded\n  birth sort last.\n* `updated` — when the person record was last touched.\n\nAny other value is a 400. Absent ⇒ a stable but arbitrary order by\nid. Sorting is applied before paging, so page 2 continues page 1.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`asc` (default, oldest first) or `desc` (newest first). A cursor is\nonly valid against the order it was minted under.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_PersonDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Conclude a new person over a set of personas",
        "operationId": "post_person",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePersonRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePersonResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePersonResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/duplicate-candidates": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Rank likely-existing duplicates of a person being entered",
        "operationId": "get_duplicate_candidates",
        "parameters": [
          {
            "name": "given",
            "in": "query",
            "description": "Given name as typed so far (whitespace-separated pieces).",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "surname",
            "in": "query",
            "description": "Surname as typed so far.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "birth",
            "in": "query",
            "description": "Birth date in GEDCOM syntax (`1832`, `ABT 1832`, `12 JAN 1900`,\n`BEF 1900`) — the same grammar every entry-form date field takes.\nUnparseable text 400s; scoring without a typed date would misrank\nand hide why.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "death",
            "in": "query",
            "description": "Death date, same syntax and same strictness as `birth`.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max candidates returned (default 8, cap 50) — an entry form wants\na short \"did you mean?\" list, not a page walk.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DuplicateCandidateDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/merge": {
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Merge two persons into one",
        "operationId": "post_person_merge",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MergePersonsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MergePersonsResultDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/{id}": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get a person's detail record by id",
        "operationId": "get_person",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/{id}/card": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get a person's orientation card (name, lifespan, immediate family, source coverage)",
        "operationId": "get_person_card",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "expand",
            "in": "query",
            "description": "Comma-separated related record sets to inline: any of `events`,\n`assertions`, `sources`. Omit for the counts-only card. Each\nexpanded list is capped at 100 rows. An unrecognised name is a\n400, never a silently unexpanded card.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonCardDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/{id}/fan-network": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get the fan-network relationship view centered on a person",
        "operationId": "get_fan_network",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "window_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "window_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "edge_types",
            "in": "query",
            "description": "Comma-separated `shared_event` / `association` / `place_co_occurrence`\n(default: all three).",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FanNetworkDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/{id}/living-status": {
      "put": {
        "tags": [
          "persons"
        ],
        "summary": "Set a person's living-status override",
        "operationId": "put_person_living_status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetLivingStatusRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/{id}/pedigree": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get a person's ancestor pedigree as Ahnentafel-numbered slots",
        "operationId": "get_pedigree",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "generations",
            "in": "query",
            "description": "How many ancestor generations to resolve, from 1 to 10 (default\n4). Anything outside that range is a 400.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PedigreeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/{id}/retract": {
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Retract a mistaken person conclusion",
        "operationId": "post_person_retract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/persons/{id}/split": {
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Split a person into two, reversing a conclusion or merge",
        "operationId": "post_person_split",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SplitPersonRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SplitPersonResultDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/project/state": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "The session brief — current investigation state as a bounded digest",
        "operationId": "get_project_state",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "The `cursor` value from a previous brief; restricts every section\nto what changed after it. Omit for the full digest.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionBriefDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/proof-arguments": {
      "get": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "List proof arguments, paginated",
        "operationId": "list_proof_arguments_route",
        "parameters": [
          {
            "name": "question",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofArgumentListResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "Create a proof argument",
        "operationId": "post_proof_argument",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordProofArgumentRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordProofArgumentResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordProofArgumentResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/proof-arguments/{id}": {
      "get": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "Get a proof argument by id",
        "operationId": "get_proof_argument_by_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofArgumentDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/proof-arguments/{id}/export": {
      "get": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "Export a proof argument",
        "operationId": "get_proof_argument_export",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofArgumentExportDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/proposals": {
      "get": {
        "tags": [
          "proposals"
        ],
        "summary": "List pending merge proposals",
        "operationId": "list_proposals",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ProposalDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "proposals"
        ],
        "summary": "Propose a merge",
        "operationId": "propose",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProposeRedactRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/proposals/{id}/approve": {
      "post": {
        "tags": [
          "proposals"
        ],
        "summary": "Approve a merge proposal",
        "operationId": "approve_proposal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "optional; when `summary_shown` is present it is recorded to the audit log",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/ApproveRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/proposals/{id}/deny": {
      "post": {
        "tags": [
          "proposals"
        ],
        "summary": "Deny a merge proposal",
        "operationId": "deny_proposal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DenyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/redactions": {
      "post": {
        "tags": [
          "redactions"
        ],
        "summary": "Redact a record",
        "operationId": "post_redaction",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedactionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedactionResultDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/relationships": {
      "post": {
        "tags": [
          "relationships"
        ],
        "summary": "Attach a parent or spouse edge between two persons",
        "operationId": "post_relationship",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RelationshipAttachRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelationshipOutcomeDto"
                }
              }
            }
          },
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelationshipOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/relationships/reparent": {
      "post": {
        "tags": [
          "relationships"
        ],
        "summary": "Replace a child's parent with a different parent",
        "operationId": "post_relationship_reparent",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RelationshipReparentRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelationshipOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/relationships/unlink": {
      "post": {
        "tags": [
          "relationships"
        ],
        "summary": "Remove a parent or spouse edge between two persons",
        "operationId": "post_relationship_unlink",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RelationshipUnlinkRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get the research log for a session",
        "operationId": "get_research_log",
        "parameters": [
          {
            "name": "question",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Add a search/source entry to the research log",
        "operationId": "add_log_entry",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddLogEntryRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log/capture": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Capture a research-log entry with evidence",
        "operationId": "post_research_log_capture",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResearchLogCaptureRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchLogCaptureResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log/entries": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "List research-log entries",
        "operationId": "get_research_log_entries",
        "parameters": [
          {
            "name": "question",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "session",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "persona",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`\"asc\"` (the default — oldest first) or `\"desc\"` (newest first).\nAnything else is a 400 rather than a silent fallback to the\ndefault. A cursor is only valid against the order it was minted\nunder, so do not change `order` mid-walk.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchLogListResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log/negative-evidence": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Record a negative-evidence research-log entry",
        "operationId": "post_log_negative_record",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogNegativeRecordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogNegativeRecordResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log/{id}": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research-log entry",
        "operationId": "get_research_log_entry",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchLogEntryDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log/{id}/details": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Append detail to a research-log entry",
        "operationId": "post_log_detail_append",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogDetailAppendRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogDetailAppendResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log/{id}/retract": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Retract a research-log entry from active views",
        "operationId": "post_log_entry_retract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/log/{id}/text": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Correct a research-log entry's text",
        "operationId": "put_log_entry_text",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogEntryTextSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/plan-items/{id}": {
      "delete": {
        "tags": [
          "research"
        ],
        "summary": "Delete a research plan item",
        "operationId": "delete_plan_item",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/plan-items/{id}/description": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Correct a research plan item's description",
        "operationId": "put_plan_item_description",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemDescriptionSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/plan-items/{id}/done": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Mark a research plan item done",
        "operationId": "put_plan_item_done",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemDoneSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanItemDoneSetResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/plan-items/{id}/reorder": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Reorder a research plan item",
        "operationId": "post_plan_item_reorder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemReorderRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "List research questions, paginated",
        "operationId": "list_research_questions_route",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "`\"open\"` / `\"closed\"`.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "subject_kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "subject_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchQuestionListResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Open a new research question",
        "operationId": "open_research_question",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenResearchQuestionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research question by id",
        "operationId": "get_research_question",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchQuestionDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/close": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Close a research question",
        "operationId": "close_research_question",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "optional; records how the lead was retired",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/CloseQuestionRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/coverage": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research question's coverage status",
        "operationId": "get_research_question_coverage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuestionCoverageResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/coverage-confirm": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Confirm reasonably-exhaustive search coverage",
        "operationId": "confirm_coverage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/plan": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research question's plan",
        "operationId": "get_research_plan",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchPlanGetResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/plan/items": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Add an item to a research question's plan",
        "operationId": "post_plan_item",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemAddRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanItemAddResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/reopen": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Reopen a retired research question (human-only)",
        "operationId": "post_research_question_reopen",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/subjects": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Set a research question's subject persons",
        "operationId": "put_question_subjects",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuestionSubjectsSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuestionDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/questions/{id}/text": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Correct a research question's text",
        "operationId": "put_research_question_text",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuestionTextSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/sessions": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Start a research session under a question",
        "operationId": "start_session",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartSessionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/research/sessions/{id}/end": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "End a research session",
        "operationId": "end_session",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/restore": {
      "post": {
        "tags": [
          "backup"
        ],
        "summary": "Restore a backup into a new tree",
        "description": "Restores one backup into a **new** tree. Requires the human token.\n\nThe restore never overwrites an existing tree, and it does not change\nwhich tree is active — switching to the restored tree is a separate,\nexplicit call to `PUT /v1/trees/active`. Every object is re-hashed\nagainst the backup manifest before a byte is written; the response\nreports how many objects and bytes were verified.",
        "operationId": "post_restore",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RestoreRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RestoredTreeDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/sessions/current": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "Get the calling connection's current session, if any",
        "operationId": "get_session_current",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "null"
                    },
                    {
                      "$ref": "#/components/schemas/SessionSummaryDto"
                    }
                  ]
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/sessions/{id}": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "Get a store session's summary by id",
        "operationId": "get_session",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSummaryDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/settled": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Settled knowledge and retired leads, in one call",
        "operationId": "get_settled",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettledKnowledgeDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/shutdown": {
      "post": {
        "tags": [
          "updates"
        ],
        "summary": "Shut down the background service",
        "description": "Stops the background service. Requires the human token — an agent\ncredential can never call it.\n\nReturns `204` immediately and then shuts down gracefully: in-flight\njobs are stopped at their own checkpoints and pending work is flushed\nbefore the process exits, so a shutdown during an import does not lose\nbuffered records. Nothing asks the user to confirm; the caller is\nexpected to have done that. Once the process is gone its discovery\nfile is gone too, so a client rediscovers the service on next start.\n\nTier: `experimental` — this is a local control-plane route for the\napp's own shell. It may change or disappear; do not build a third-party\nintegration on it.",
        "operationId": "post_shutdown",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "204": {
            "description": "graceful shutdown initiated; discovery files are removed and the process exits"
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "experimental"
      }
    },
    "/v1/sources": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "List sources, paginated",
        "description": "Every source in the tree, cursor-paginated, with `q=` filtering on the\ntitle. These rows are deliberately light — fetch\n`GET /v1/sources/{id}` for a source's citation and full detail, and\n`GET /v1/sources/{id}/cited-by` for the claims resting on it.",
        "operationId": "list_sources",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Case-insensitive substring match on the source title. Applied\nbefore pagination, so `total` counts the matches and page 2 of a\nfiltered list is page 2 of the matches. This is a plain substring\nmatch, not the relevance-ranked search `GET /v1/persons?q=` runs.\nAbsent or blank ⇒ unfiltered.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_SourceListItemDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "sources"
        ],
        "summary": "Create a source",
        "operationId": "post_source",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSourceRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/sources/from-template": {
      "post": {
        "tags": [
          "sources"
        ],
        "summary": "Create a source from a citation template",
        "operationId": "post_source_from_template",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SourceFromTemplateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceFromTemplateResultDto"
                }
              }
            }
          },
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceFromTemplateResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/sources/{id}": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "Get a source by id",
        "operationId": "get_source",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/sources/{id}/attach-blob": {
      "post": {
        "tags": [
          "sources"
        ],
        "summary": "Attach an existing blob to a source",
        "operationId": "attach_source_blob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AttachSourceBlobRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/sources/{id}/cited-by": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "List the claims that cite a source, paginated",
        "description": "The inverse of the citation edge every assertion carries: given a\nsource, which claims rest on it. This is what turns the `claim_count`\non a source into something you can actually enumerate — unlike\n`GET /v1/assertions`, it needs no persona to start from.\n\nAlways paginated: a source created by a GEDCOM import can carry\nthousands of claims.",
        "operationId": "list_source_cited_by",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_SourceCitedClaimDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/stats": {
      "get": {
        "tags": [
          "trees"
        ],
        "summary": "Whole-tree orientation statistics",
        "operationId": "get_tree_stats",
        "parameters": [
          {
            "name": "top_surnames",
            "in": "query",
            "description": "Surname-frequency list cap (default 10, max 50).",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreeStatsDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/telemetry/config": {
      "get": {
        "tags": [
          "telemetry"
        ],
        "summary": "Get the telemetry export consent/config record",
        "operationId": "get_telemetry_config",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryConfigDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "put": {
        "tags": [
          "telemetry"
        ],
        "summary": "Set the telemetry export consent/config record",
        "operationId": "put_telemetry_config",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PutTelemetryConfigRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryConfigDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/telemetry/events": {
      "post": {
        "tags": [
          "telemetry"
        ],
        "summary": "Record a telemetry event",
        "operationId": "post_telemetry_event",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostTelemetryEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryEventQueuedDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/telemetry/queue": {
      "get": {
        "tags": [
          "telemetry"
        ],
        "summary": "Get the local telemetry queue",
        "operationId": "get_telemetry_queue",
        "responses": {
          "200": {
            "description": "newline-delimited JSON: the raw queued + pending-segment telemetry Envelope records, oldest-first"
          }
        },
        "x-heartwood-tier": "preview"
      },
      "delete": {
        "tags": [
          "telemetry"
        ],
        "summary": "Purge the local telemetry queue",
        "operationId": "delete_telemetry_queue",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryQueuePurgedDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees": {
      "get": {
        "tags": [
          "trees"
        ],
        "summary": "List trees in this store",
        "operationId": "list_trees",
        "parameters": [
          {
            "name": "include_archived",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "boolean",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TreeDto"
                  }
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "trees"
        ],
        "summary": "Create a new tree",
        "operationId": "create_tree",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTreeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/active": {
      "get": {
        "tags": [
          "trees"
        ],
        "summary": "Get the currently active tree",
        "operationId": "get_active_tree",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveTreeDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "put": {
        "tags": [
          "trees"
        ],
        "summary": "Set the currently active tree",
        "operationId": "put_active_tree",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetActiveTreeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveTreeDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/claim": {
      "post": {
        "tags": [
          "trees"
        ],
        "summary": "Name the tree a first-run reader starts in",
        "description": "The daemon boots with a provisional tree whose name no reader may see,\nso onboarding claims it rather than leaving an unexplained spare\nbehind. But that tree is reachable by every other route while the\nblocking dialog sits open — the dialog's own copy invites a connected\nassistant to start researching — and a tree that has acquired a family\nline is no longer provisional. Deciding claim-vs-create in the caller\nmeans re-deciding it in every future caller; deciding it here means the\nanswer cannot go stale between the decision and the write.\n\nThe returned tree is made active: this is the tree the reader was told\nthey now have, so it is the one they must be in.",
        "operationId": "claim_tree",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClaimTreeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}": {
      "get": {
        "tags": [
          "trees"
        ],
        "summary": "Get a tree by id",
        "operationId": "get_tree",
        "parameters": [
          {
            "name": "tree_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreeDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "patch": {
        "tags": [
          "trees"
        ],
        "summary": "Rename or archive/unarchive a tree",
        "operationId": "patch_tree",
        "parameters": [
          {
            "name": "tree_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchTreeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreeDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "List assertions, paginated",
        "description": "The claims recorded against one persona, cursor-paginated. `persona`\nis **required** — this route is scoped to a persona, not to a person,\nso start from `GET /v1/persons/{id}` if all you have is a person id.\n\n`kind=`, `state=` and `source=` narrow the set before it is paginated,\nso `total` counts the matches. An unrecognised query parameter is a\n`400` rather than being ignored.",
        "operationId": "list_assertions",
        "parameters": [
          {
            "name": "persona",
            "in": "query",
            "description": "**Required.** Assertions are listed per persona, so a caller\nholding a person id resolves it first — `GET /v1/persons/{id}`\nreturns that person's persona ids.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "Keep only assertions whose `kind` equals this string exactly (e.g.\n`\"fact\"`) — the same vocabulary a returned assertion's `kind` uses.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "Keep only assertions whose epistemic status equals this string\nexactly (e.g. `\"confirmed\"`). The parameter is spelled `state`; the\nfield it filters is spelled `status` in the response body. Same\nvocabulary, different name.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Keep only assertions whose `source` equals this source id.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "How to order the results. `\"created\"` — by the assertion's\n`created_at_ms` — is the only value accepted today; anything else\nis a 400. Absent ⇒ ordering by id.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`asc` (default, oldest first) or `desc` (newest first). A cursor is\nonly valid against the order it was minted under.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_AssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/capture": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Capture a new assertion with its evidence",
        "operationId": "capture_assertion",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CaptureRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/transcription-claim": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Claim a transcription of a source blob",
        "operationId": "post_transcription_claim",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClaimTranscriptionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/unsourced": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "List assertions awaiting human evaluation, paginated",
        "operationId": "list_unsourced_assertions",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_UnsourcedAssertionDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/unsourced/tally": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "Count how unsourced-surface claims split into sourced and needs-source",
        "description": "A client cannot get this by reading the listing's page `total`: that\nfigure counts both halves, so a surface labelling it \"unsourced\" says\nthe whole tree is unsourced, including the claims it renders as\nsourced on the very same screen. `total` here is the sum of the two\nhalves, never an independent count.",
        "operationId": "tally_unsourced_assertions",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourcingTallyDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/{id}": {
      "get": {
        "tags": [
          "assertions"
        ],
        "summary": "Get an assertion by id",
        "operationId": "get_assertion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssertionDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/{id}/attach-evidence": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Attach evidence to an existing assertion",
        "operationId": "attach_evidence",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AttachEvidenceRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/{id}/confirm": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Confirm a pending assertion",
        "operationId": "confirm_assertion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/{id}/mark-fabricated": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Mark an assertion as fabricated",
        "operationId": "mark_fabricated",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/{id}/refute": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Refute an assertion",
        "operationId": "refute_assertion",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assertions/{id}/supersede": {
      "post": {
        "tags": [
          "assertions"
        ],
        "summary": "Supersede an assertion with a replacement",
        "operationId": "post_assertion_supersede",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SupersedeAssertionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/assignments/{id}": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "Get a recorded assignment by id",
        "operationId": "get_assignment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignmentDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/associations": {
      "post": {
        "tags": [
          "associations"
        ],
        "summary": "Create an association between persons",
        "operationId": "post_association",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordAssociationRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssociationIdDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/audit": {
      "get": {
        "tags": [
          "audit"
        ],
        "summary": "Get the audit trail",
        "operationId": "get_audit",
        "parameters": [
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_AuditEntryDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/backup": {
      "post": {
        "tags": [
          "trees"
        ],
        "summary": "Back up a tree to the configured location (async)",
        "description": "Backs up a tree to the location configured for this installation.\nEither actor class may call it — backing up is never destructive.\n\n**There is no request body and no destination parameter.** A backup\nalways writes to the one location set through the human-only\n`PUT /v1/backup/config`. That is deliberate: a per-call destination\nwould turn this into a \"write bytes to any path I name\" primitive\nreachable with an agent credential. If no location is configured the\nroute refuses with `409` rather than choosing one.\n\n**Asynchronous.** `404` (unknown tree) and `409` (no location\nconfigured, or backups disabled) are synchronous refusals; otherwise\nthe copy runs as a job and the route returns `202` with a `job_id`.\nPoll `GET /v1/jobs/{job_id}` or watch `job.progress` events; the\nphases are `staging`, `applying`, `finalizing`. Once the backup\ncommits, older backups beyond the configured retention are pruned; a\nprune that fails is logged and does not fail the backup.",
        "operationId": "post_backup_tree",
        "parameters": [
          {
            "name": "tree_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptedJobDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/blobs": {
      "post": {
        "tags": [
          "blobs"
        ],
        "summary": "Upload a blob",
        "description": "Stores raw bytes and returns their content address. Send the bytes as\nthe request body with `Content-Type: application/octet-stream`; the\naddress that comes back is what every other route means by a blob\nreference (bare hex, 64 characters).\n\nThe body is streamed rather than buffered, so upload size is bounded\nby the ceiling below rather than by memory. A body that exceeds the\nceiling is refused with `413` while it is still arriving, and the\npartial upload is discarded — a rejected upload leaves nothing\nbehind.",
        "operationId": "post_blob",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "raw bytes, streamed",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 0
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlobRefDto"
                }
              }
            }
          },
          "413": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/blobs/{address}": {
      "get": {
        "tags": [
          "blobs"
        ],
        "summary": "Download a blob by content address",
        "operationId": "get_blob",
        "parameters": [
          {
            "name": "address",
            "in": "path",
            "description": "sha256:<hex> content address",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "application/octet-stream blob bytes"
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/citation-templates": {
      "get": {
        "tags": [
          "citations"
        ],
        "summary": "List available citation templates",
        "operationId": "get_citation_templates",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitationTemplatesListDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/citations": {
      "post": {
        "tags": [
          "citations"
        ],
        "summary": "Create a general-purpose citation from typed elements",
        "operationId": "post_citation",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCitationRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateCitationResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/citations/render": {
      "post": {
        "tags": [
          "citations"
        ],
        "summary": "Render a citation to formatted text",
        "operationId": "post_citation_render",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CitationRenderRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitationRenderResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/citations/{id}/elements": {
      "put": {
        "tags": [
          "citations"
        ],
        "summary": "Set a citation's structured elements",
        "operationId": "put_citation_elements",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetCitationElementsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationElementsResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationElementsResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/citations/{id}/source-type": {
      "put": {
        "tags": [
          "citations"
        ],
        "summary": "Record what kind of record a source is",
        "operationId": "put_citation_source_type",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetCitationSourceTypeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationSourceTypeResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetCitationSourceTypeResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/conflicts": {
      "get": {
        "tags": [
          "conflicts"
        ],
        "summary": "List detected conflicts",
        "description": "Each row names the persona or person it belongs to and carries the\nclaims that disagree, each already joined to the source that made it\n— so a caller can describe the dispute without fetching anything\nelse. Filter with `persona` or `person` to get only one subject's\nconflicts; pass neither for the whole tree.",
        "operationId": "list_conflicts",
        "parameters": [
          {
            "name": "persona",
            "in": "query",
            "description": "Only conflicts recorded against this persona.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "person",
            "in": "query",
            "description": "Only conflicts about this person's identity, plus every conflict\non a persona this person concludes over.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ConflictDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/conflicts/{id}/resolution": {
      "post": {
        "tags": [
          "conflicts"
        ],
        "summary": "Resolve a conflict with a proof argument",
        "description": "Returns the updated conflict, now closed. A conflict is settled once\nand only once: resolving one that is already settled is a `409`, and\nnaming a conflict or an argument that does not exist is a `404`.",
        "operationId": "post_conflict_resolution",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResolveConflictRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConflictDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/delete": {
      "post": {
        "tags": [
          "trees"
        ],
        "summary": "Permanently delete a tree (async)",
        "description": "Deletes a whole tree — its history, its stored files and every\nsnapshot of it. Requires the human token: an agent credential can archive a\ntree (which is reversible) but can never delete one.\n\n**Asynchronous.** A tree can hold tens of thousands of records, so the\nwork runs as a job. Validation is synchronous — `404` for an unknown\ntree, `409` if it is the active tree (switch away first) — and on\nsuccess the route returns `202` with a `job_id`. Poll\n`GET /v1/jobs/{job_id}` for progress, or watch `job.progress` events\non the SSE stream; the phases are `closing` then `removing`. The job\nre-validates before it runs, so a tree that became active in the\nmeantime fails the job rather than being deleted anyway.\n\nDeletion is recoverable for a window: the tree's directory is moved to\na trash area rather than destroyed in place, and trash entries are\npurged 30 days later.",
        "operationId": "post_delete_tree",
        "parameters": [
          {
            "name": "tree_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptedJobDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/events": {
      "post": {
        "tags": [
          "events"
        ],
        "summary": "Create a life event",
        "operationId": "post_event",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordLifeEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordedLifeEventDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/events/stream": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Subscribe to the live change-event stream (SSE)",
        "description": "A `text/event-stream` of change notices, so a client can refetch what\nmoved instead of polling. The feed is best-effort and has no replay: a\nclient that connects late has missed the earlier events and catches up\nby re-reading state, not by rewinding the stream.\n\nThe two mounts of this route behave differently, deliberately:\n\n* `/v1/trees/{tree_id}/events/stream` stays pinned to that one tree\n  for the life of the connection.\n* `/v1/events/stream` follows whichever tree is currently active. When\n  the active tree changes it re-subscribes to the new tree and emits a\n  `tree.active_changed` event, which is a client's cue to refetch\n  once.",
        "operationId": "sse_stream",
        "responses": {
          "200": {
            "description": "text/event-stream of change notices"
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/events/{id}": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Get a life event by id",
        "operationId": "get_event",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LifeEventDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/events/{id}/corrections": {
      "post": {
        "tags": [
          "events"
        ],
        "summary": "Correct a life event's details",
        "operationId": "post_event_correction",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CorrectEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CorrectionResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CorrectionResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/events/{id}/participants": {
      "post": {
        "tags": [
          "events"
        ],
        "summary": "Add a participant to a life event",
        "operationId": "post_event_participant",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddParticipantRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipationIdDto"
                }
              }
            }
          },
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipationIdDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/fan/query": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Query the fan-network relationship view by parameters",
        "operationId": "get_fan_network_query",
        "parameters": [
          {
            "name": "persona_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "person_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "window_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "window_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "edge_types",
            "in": "query",
            "description": "Comma-separated `shared_event` / `association` / `place_co_occurrence`\n(default: all three).",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FanNetworkDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/gedcom/export": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Export the tree to GEDCOM (family file) format",
        "operationId": "post_gedcom_export",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GedcomExportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GedcomExportResultDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/gedcom/loss-reports": {
      "get": {
        "tags": [
          "gedcom"
        ],
        "summary": "Get a GEDCOM import's loss report",
        "operationId": "get_gedcom_loss_report",
        "parameters": [
          {
            "name": "report_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LossReportDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/gedcom/validate": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Validate a GEDCOM (family file) without importing",
        "operationId": "post_gedcom_validate",
        "parameters": [
          {
            "name": "version_hint",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "raw GEDCOM bytes",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 0
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LossReportDto"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/home-person": {
      "get": {
        "tags": [
          "trees"
        ],
        "summary": "Get a tree's home person (the pedigree's default anchor)",
        "operationId": "get_home_person",
        "parameters": [
          {
            "name": "tree_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HomePersonDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "put": {
        "tags": [
          "trees"
        ],
        "summary": "Set or clear a tree's home person",
        "operationId": "put_home_person",
        "parameters": [
          {
            "name": "tree_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HomePersonDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HomePersonDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/imports": {
      "get": {
        "tags": [
          "gedcom"
        ],
        "summary": "List import runs",
        "operationId": "get_imports",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ImportRecordDto"
                  }
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/imports/gedcom": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Import a GEDCOM (family file) into the tree",
        "operationId": "post_import_gedcom",
        "parameters": [
          {
            "name": "filename",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "allow_duplicate",
            "in": "query",
            "description": "Default `false`. Uploading bytes identical to a previous import\nrefuses synchronously with `409 duplicate-import`, before any job\nis spawned. Set this to import the same file a second time on\npurpose.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "raw GEDCOM bytes",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 0
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptedJobDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/imports/{import_key}/undo": {
      "post": {
        "tags": [
          "gedcom"
        ],
        "summary": "Undo a GEDCOM import",
        "operationId": "post_import_undo",
        "parameters": [
          {
            "name": "import_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UndoImportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptedJobDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/jobs": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "List background jobs",
        "operationId": "list_jobs",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_JobDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/jobs/{id}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "Get a background job's status",
        "operationId": "get_job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/jobs/{id}/cancel": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Cancel a background job",
        "operationId": "cancel_job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "202": {
            "description": "cancel requested"
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/labels": {
      "get": {
        "tags": [
          "labels"
        ],
        "summary": "Resolve entity ids to display labels",
        "description": "Turns a batch of entity ids into the names a human reads. Reads that\nreturn ids only — `GET /v1/audit` above all — pair with this route so\na caller can render a readable timeline in one extra call instead of\none call per row.\n\nIds are globally unique across every entity kind, so no kind hint is\nneeded or accepted. An id that resolves to nothing is **omitted from\nthe response** rather than reported as an error: a partly-resolving\nbatch is the normal case (an entity may have been redacted or\nremoved), and the response tells you what it found, in any order.\n\nEvery label returned here is one the same caller could already read\nfrom `GET /v1/persons/{id}` and its siblings; this route is a\nconvenience, not a new grant.",
        "operationId": "get_labels",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "description": "Comma-separated entity ids.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EntityLabelDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/media/unresolved": {
      "get": {
        "tags": [
          "media"
        ],
        "summary": "List media this machine has not read in, by file name",
        "description": "No folder is reported and none is accepted: a caller supplies bytes by\nuploading them to `POST /v1/blobs` and naming the returned address to\n`POST /v1/media/{id}/resolve`. Returns every row in one response.",
        "operationId": "get_unresolved_media",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/UnresolvedMediaDto"
                  }
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/media/{id}/resolve": {
      "post": {
        "tags": [
          "media"
        ],
        "summary": "Resolve a media object from an already-uploaded blob",
        "description": "The blob becomes this tree's own copy, so whatever the caller read it\nfrom may afterwards be moved, renamed or deleted with no effect here.\nAnswers `200` with `status: \"already_resolved\"` when the file already\nread from these exact bytes — a no-op rather than a refusal. Naming a\n*different* blob for a file that already has one is a correction, not\na no-op: it lands, and `status` is `resolved`. That is what makes\nattaching the wrong file recoverable, since `media.resolved.v1` has no\nundo route.",
        "operationId": "post_media_resolve",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResolveMediaRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaResolveResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/notes": {
      "get": {
        "tags": [
          "notes"
        ],
        "summary": "List notes",
        "operationId": "get_notes",
        "parameters": [
          {
            "name": "target",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NoteDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "notes"
        ],
        "summary": "Create a note",
        "operationId": "post_note",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddNoteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteIdDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/pending": {
      "get": {
        "tags": [
          "pending"
        ],
        "summary": "List assertions in the pending lane",
        "description": "The review queue: assertions an agent has proposed that no human has\naccepted or rejected yet. Cursor-paginated like every other collection\nroute.\n\n`source=` and `proposer=` narrow the set; `sort=created` with `order=`\ncontrols ordering; `group_by=source|proposer` switches the response\nfrom a flat page to buckets — see the response schema for how the two\nshapes differ.\n\nConfirming or rejecting an item removes it from this queue: use\n`POST /v1/assertions/{id}/confirm`, `.../refute`, or\n`POST /v1/pending/bulk-confirm` for a whole batch.",
        "operationId": "get_pending",
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "description": "Keep only pending items whose `source` equals this source id.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "proposer",
            "in": "query",
            "description": "Keep only pending items whose `proposer` equals this string — the\nmodel identifier the creating agent reported for itself.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "How to order the results. `\"created\"` is the only value accepted\ntoday.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`asc` (default, oldest first) or `desc` (newest first). A cursor is\nonly valid against the order it was minted under.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "group_by",
            "in": "query",
            "description": "Return the (already filtered and sorted) result set bucketed by\n`source` or by `proposer` instead of as a flat page. See this\nroute's own description for how that changes the response shape.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PendingPageDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/pending/bulk-confirm": {
      "post": {
        "tags": [
          "pending"
        ],
        "summary": "Bulk-confirm pending assertions",
        "operationId": "bulk_confirm_pending",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkConfirmRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkConfirmResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/pending/{id}/defer": {
      "post": {
        "tags": [
          "pending"
        ],
        "summary": "Defer a pending assertion",
        "operationId": "defer_pending",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/pending/{id}/undefer": {
      "post": {
        "tags": [
          "pending"
        ],
        "summary": "Undefer a previously-deferred pending assertion",
        "operationId": "undefer_pending",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/personas": {
      "post": {
        "tags": [
          "personas"
        ],
        "summary": "Create a persona",
        "operationId": "post_persona",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePersonaRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/personas/{id}": {
      "get": {
        "tags": [
          "personas"
        ],
        "summary": "Get a persona by id",
        "operationId": "get_persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonaDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/personas/{id}/name-pieces": {
      "put": {
        "tags": [
          "personas"
        ],
        "summary": "Set a persona's structured name pieces",
        "operationId": "put_name_pieces",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetNamePiecesRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "List persons, paginated",
        "description": "Every person in the tree, cursor-paginated. `q=` searches names,\n`sort=`/`order=` control ordering, and `limit=`/`cursor=` page through\nthe result — see each parameter for the details.\n\nA query parameter this route does not recognise is a `400`, never\nsilently ignored, so a typo in a filter name fails loudly instead of\nreturning an unfiltered page that looks right.",
        "operationId": "list_persons",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Free-text person-name search. Matches a name prefix first and falls\nback to a substring match when no prefix hits. Results come back in\nrelevance order, so a `sort`/`order` given alongside `q` is accepted\nbut ignored rather than rejected. Absent ⇒ no name filter.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "How to order the results:\n\n* `name` — the person's display name.\n* `surname` — the display name's last whitespace-separated token,\n  then the full name.\n* `birth` — earliest recorded birth year; people with no recorded\n  birth sort last.\n* `updated` — when the person record was last touched.\n\nAny other value is a 400. Absent ⇒ a stable but arbitrary order by\nid. Sorting is applied before paging, so page 2 continues page 1.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`asc` (default, oldest first) or `desc` (newest first). A cursor is\nonly valid against the order it was minted under.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_PersonDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Conclude a new person over a set of personas",
        "operationId": "post_person",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePersonRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePersonResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePersonResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/duplicate-candidates": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Rank likely-existing duplicates of a person being entered",
        "operationId": "get_duplicate_candidates",
        "parameters": [
          {
            "name": "given",
            "in": "query",
            "description": "Given name as typed so far (whitespace-separated pieces).",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "surname",
            "in": "query",
            "description": "Surname as typed so far.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "birth",
            "in": "query",
            "description": "Birth date in GEDCOM syntax (`1832`, `ABT 1832`, `12 JAN 1900`,\n`BEF 1900`) — the same grammar every entry-form date field takes.\nUnparseable text 400s; scoring without a typed date would misrank\nand hide why.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "death",
            "in": "query",
            "description": "Death date, same syntax and same strictness as `birth`.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max candidates returned (default 8, cap 50) — an entry form wants\na short \"did you mean?\" list, not a page walk.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DuplicateCandidateDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/merge": {
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Merge two persons into one",
        "operationId": "post_person_merge",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MergePersonsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MergePersonsResultDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/{id}": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get a person's detail record by id",
        "operationId": "get_person",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/{id}/card": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get a person's orientation card (name, lifespan, immediate family, source coverage)",
        "operationId": "get_person_card",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "expand",
            "in": "query",
            "description": "Comma-separated related record sets to inline: any of `events`,\n`assertions`, `sources`. Omit for the counts-only card. Each\nexpanded list is capped at 100 rows. An unrecognised name is a\n400, never a silently unexpanded card.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonCardDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/{id}/fan-network": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get the fan-network relationship view centered on a person",
        "operationId": "get_fan_network",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "window_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "window_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "edge_types",
            "in": "query",
            "description": "Comma-separated `shared_event` / `association` / `place_co_occurrence`\n(default: all three).",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FanNetworkDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/{id}/living-status": {
      "put": {
        "tags": [
          "persons"
        ],
        "summary": "Set a person's living-status override",
        "operationId": "put_person_living_status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetLivingStatusRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/{id}/pedigree": {
      "get": {
        "tags": [
          "persons"
        ],
        "summary": "Get a person's ancestor pedigree as Ahnentafel-numbered slots",
        "operationId": "get_pedigree",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "generations",
            "in": "query",
            "description": "How many ancestor generations to resolve, from 1 to 10 (default\n4). Anything outside that range is a 400.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PedigreeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/{id}/retract": {
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Retract a mistaken person conclusion",
        "operationId": "post_person_retract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/persons/{id}/split": {
      "post": {
        "tags": [
          "persons"
        ],
        "summary": "Split a person into two, reversing a conclusion or merge",
        "operationId": "post_person_split",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SplitPersonRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SplitPersonResultDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/project/state": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "The session brief — current investigation state as a bounded digest",
        "operationId": "get_project_state",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "The `cursor` value from a previous brief; restricts every section\nto what changed after it. Omit for the full digest.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionBriefDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/proof-arguments": {
      "get": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "List proof arguments, paginated",
        "operationId": "list_proof_arguments_route",
        "parameters": [
          {
            "name": "question",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofArgumentListResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "Create a proof argument",
        "operationId": "post_proof_argument",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordProofArgumentRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordProofArgumentResultDto"
                }
              }
            }
          },
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordProofArgumentResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/proof-arguments/{id}": {
      "get": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "Get a proof argument by id",
        "operationId": "get_proof_argument_by_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofArgumentDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/proof-arguments/{id}/export": {
      "get": {
        "tags": [
          "proof-arguments"
        ],
        "summary": "Export a proof argument",
        "operationId": "get_proof_argument_export",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofArgumentExportDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/proposals": {
      "get": {
        "tags": [
          "proposals"
        ],
        "summary": "List pending merge proposals",
        "operationId": "list_proposals",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ProposalDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "proposals"
        ],
        "summary": "Propose a merge",
        "operationId": "propose",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProposeRedactRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/proposals/{id}/approve": {
      "post": {
        "tags": [
          "proposals"
        ],
        "summary": "Approve a merge proposal",
        "operationId": "approve_proposal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "optional; when `summary_shown` is present it is recorded to the audit log",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/ApproveRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/proposals/{id}/deny": {
      "post": {
        "tags": [
          "proposals"
        ],
        "summary": "Deny a merge proposal",
        "operationId": "deny_proposal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DenyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/redactions": {
      "post": {
        "tags": [
          "redactions"
        ],
        "summary": "Redact a record",
        "operationId": "post_redaction",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedactionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedactionResultDto"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/relationships": {
      "post": {
        "tags": [
          "relationships"
        ],
        "summary": "Attach a parent or spouse edge between two persons",
        "operationId": "post_relationship",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RelationshipAttachRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelationshipOutcomeDto"
                }
              }
            }
          },
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelationshipOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/relationships/reparent": {
      "post": {
        "tags": [
          "relationships"
        ],
        "summary": "Replace a child's parent with a different parent",
        "operationId": "post_relationship_reparent",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RelationshipReparentRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelationshipOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/relationships/unlink": {
      "post": {
        "tags": [
          "relationships"
        ],
        "summary": "Remove a parent or spouse edge between two persons",
        "operationId": "post_relationship_unlink",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RelationshipUnlinkRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get the research log for a session",
        "operationId": "get_research_log",
        "parameters": [
          {
            "name": "question",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Add a search/source entry to the research log",
        "operationId": "add_log_entry",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddLogEntryRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log/capture": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Capture a research-log entry with evidence",
        "operationId": "post_research_log_capture",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResearchLogCaptureRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchLogCaptureResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log/entries": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "List research-log entries",
        "operationId": "get_research_log_entries",
        "parameters": [
          {
            "name": "question",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "session",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "persona",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "`\"asc\"` (the default — oldest first) or `\"desc\"` (newest first).\nAnything else is a 400 rather than a silent fallback to the\ndefault. A cursor is only valid against the order it was minted\nunder, so do not change `order` mid-walk.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchLogListResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log/negative-evidence": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Record a negative-evidence research-log entry",
        "operationId": "post_log_negative_record",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogNegativeRecordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogNegativeRecordResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log/{id}": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research-log entry",
        "operationId": "get_research_log_entry",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchLogEntryDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log/{id}/details": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Append detail to a research-log entry",
        "operationId": "post_log_detail_append",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogDetailAppendRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogDetailAppendResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log/{id}/retract": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Retract a research-log entry from active views",
        "operationId": "post_log_entry_retract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/log/{id}/text": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Correct a research-log entry's text",
        "operationId": "put_log_entry_text",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogEntryTextSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/plan-items/{id}": {
      "delete": {
        "tags": [
          "research"
        ],
        "summary": "Delete a research plan item",
        "operationId": "delete_plan_item",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/plan-items/{id}/description": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Correct a research plan item's description",
        "operationId": "put_plan_item_description",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemDescriptionSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/plan-items/{id}/done": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Mark a research plan item done",
        "operationId": "put_plan_item_done",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemDoneSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanItemDoneSetResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/plan-items/{id}/reorder": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Reorder a research plan item",
        "operationId": "post_plan_item_reorder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemReorderRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "List research questions, paginated",
        "operationId": "list_research_questions_route",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "`\"open\"` / `\"closed\"`.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "subject_kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "subject_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchQuestionListResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Open a new research question",
        "operationId": "open_research_question",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenResearchQuestionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research question by id",
        "operationId": "get_research_question",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchQuestionDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/close": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Close a research question",
        "operationId": "close_research_question",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "description": "optional; records how the lead was retired",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/CloseQuestionRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/coverage": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research question's coverage status",
        "operationId": "get_research_question_coverage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuestionCoverageResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/coverage-confirm": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Confirm reasonably-exhaustive search coverage",
        "operationId": "confirm_coverage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/plan": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Get a research question's plan",
        "operationId": "get_research_plan",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchPlanGetResponseDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/plan/items": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Add an item to a research question's plan",
        "operationId": "post_plan_item",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanItemAddRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanItemAddResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/reopen": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Reopen a retired research question (human-only)",
        "operationId": "post_research_question_reopen",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/subjects": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Set a research question's subject persons",
        "operationId": "put_question_subjects",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuestionSubjectsSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuestionDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/questions/{id}/text": {
      "put": {
        "tags": [
          "research"
        ],
        "summary": "Correct a research question's text",
        "operationId": "put_research_question_text",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuestionTextSetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/sessions": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "Start a research session under a question",
        "operationId": "start_session",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartSessionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/research/sessions/{id}/end": {
      "post": {
        "tags": [
          "research"
        ],
        "summary": "End a research session",
        "operationId": "end_session",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/root-suggestions": {
      "get": {
        "tags": [
          "trees"
        ],
        "summary": "Suggest people this tree's pedigree could start from",
        "operationId": "get_root_suggestions",
        "parameters": [
          {
            "name": "tree_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RootSuggestionsDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/sessions/current": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "Get the calling connection's current session, if any",
        "operationId": "get_session_current",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "null"
                    },
                    {
                      "$ref": "#/components/schemas/SessionSummaryDto"
                    }
                  ]
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/sessions/{id}": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "Get a store session's summary by id",
        "operationId": "get_session",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSummaryDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/settled": {
      "get": {
        "tags": [
          "research"
        ],
        "summary": "Settled knowledge and retired leads, in one call",
        "operationId": "get_settled",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettledKnowledgeDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/sources": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "List sources, paginated",
        "description": "Every source in the tree, cursor-paginated, with `q=` filtering on the\ntitle. These rows are deliberately light — fetch\n`GET /v1/sources/{id}` for a source's citation and full detail, and\n`GET /v1/sources/{id}/cited-by` for the claims resting on it.",
        "operationId": "list_sources",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Case-insensitive substring match on the source title. Applied\nbefore pagination, so `total` counts the matches and page 2 of a\nfiltered list is page 2 of the matches. This is a plain substring\nmatch, not the relevance-ranked search `GET /v1/persons?q=` runs.\nAbsent or blank ⇒ unfiltered.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_SourceListItemDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "post": {
        "tags": [
          "sources"
        ],
        "summary": "Create a source",
        "operationId": "post_source",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSourceRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/sources/from-template": {
      "post": {
        "tags": [
          "sources"
        ],
        "summary": "Create a source from a citation template",
        "operationId": "post_source_from_template",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SourceFromTemplateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceFromTemplateResultDto"
                }
              }
            }
          },
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceFromTemplateResultDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/sources/{id}": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "Get a source by id",
        "operationId": "get_source",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceDto"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/sources/{id}/attach-blob": {
      "post": {
        "tags": [
          "sources"
        ],
        "summary": "Attach an existing blob to a source",
        "operationId": "attach_source_blob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AttachSourceBlobRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/sources/{id}/cited-by": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "List the claims that cite a source, paginated",
        "description": "The inverse of the citation edge every assertion carries: given a\nsource, which claims rest on it. This is what turns the `claim_count`\non a source into something you can actually enumerate — unlike\n`GET /v1/assertions`, it needs no persona to start from.\n\nAlways paginated: a source created by a GEDCOM import can carry\nthousands of claims.",
        "operationId": "list_source_cited_by",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return. Clamped to `1..=500`; defaults to 100 when\nabsent. A value outside the range is clamped, not rejected.",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next_cursor` from the previous page. Omit it for the first\npage. Treat it as opaque, and keep every other query parameter\nidentical while walking — a cursor is only valid against the query\nthat minted it.",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_SourceCitedClaimDto"
                }
              }
            }
          },
          "400": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/stats": {
      "get": {
        "tags": [
          "trees"
        ],
        "summary": "Whole-tree orientation statistics",
        "operationId": "get_tree_stats",
        "parameters": [
          {
            "name": "top_surnames",
            "in": "query",
            "description": "Surname-frequency list cap (default 10, max 50).",
            "required": false,
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "format": "int32",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreeStatsDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/undo": {
      "post": {
        "tags": [
          "undo"
        ],
        "summary": "Undo the target edit, or the most recent undoable human edit",
        "operationId": "post_undo",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UndoRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UndoResponse"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/trees/{tree_id}/undo/status": {
      "get": {
        "tags": [
          "undo"
        ],
        "summary": "The caller's last edit and what a no-target undo would target",
        "operationId": "get_undo_status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UndoStatusResponse"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/undo": {
      "post": {
        "tags": [
          "undo"
        ],
        "summary": "Undo the target edit, or the most recent undoable human edit",
        "operationId": "post_undo",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UndoRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UndoResponse"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/undo/status": {
      "get": {
        "tags": [
          "undo"
        ],
        "summary": "The caller's last edit and what a no-target undo would target",
        "operationId": "get_undo_status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UndoStatusResponse"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/update/check": {
      "post": {
        "tags": [
          "updates"
        ],
        "summary": "Check for an available update",
        "operationId": "post_update_check",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "202": {
            "description": "check started; observe via status/SSE"
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/update/restart": {
      "post": {
        "tags": [
          "updates"
        ],
        "summary": "Restart to apply a downloaded update",
        "operationId": "post_update_restart",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "responses": {
          "202": {
            "description": "helper spawned; the app is shutting down to swap"
          },
          "409": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/update/settings": {
      "get": {
        "tags": [
          "updates"
        ],
        "summary": "Get the auto-updater's settings",
        "operationId": "get_update_settings",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateSettingsDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      },
      "put": {
        "tags": [
          "updates"
        ],
        "summary": "Set the auto-updater's settings",
        "operationId": "put_update_settings",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "description": "Reserved for future request deduplication. **Not deduplicated today** — the key is accepted and shape-validated (200 characters maximum, printable non-whitespace ASCII, or the request is rejected), but a retried request carrying the same key applies a second time. Do not rely on it for at-most-once delivery yet. It exists now so that real deduplication can be turned on later without changing this API.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "idempotency-key",
              "maxLength": 200,
              "minLength": 1
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSettingsDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateSettingsDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    },
    "/v1/update/status": {
      "get": {
        "tags": [
          "updates"
        ],
        "summary": "Get the auto-updater's current status",
        "operationId": "get_update_status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateStatusDto"
                }
              }
            }
          }
        },
        "x-heartwood-tier": "preview"
      }
    }
  },
  "components": {
    "schemas": {
      "AcceptedJobDto": {
        "type": "object",
        "description": "A `202` response for work that runs asynchronously: a job id and\nnothing else. Poll `GET /v1/jobs/{job_id}` for progress and the final\nresult — the outcome is not available yet at the moment this is\nreturned.",
        "required": [
          "job_id"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ActiveTreeDto": {
        "type": "object",
        "required": [
          "tree_id"
        ],
        "properties": {
          "tree_id": {
            "type": "string"
          }
        }
      },
      "AddLogEntryRequest": {
        "type": "object",
        "required": [
          "session",
          "search_context"
        ],
        "properties": {
          "search_context": {
            "type": "string"
          },
          "session": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "AddNoteRequest": {
        "type": "object",
        "description": "`POST /v1/notes`'s request body. A note is commentary attached to\nsomething, not evidence, so — unlike a capture — it needs no research\ncontext and creates no claim.",
        "required": [
          "text"
        ],
        "properties": {
          "anchors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NoteAnchorDto"
            }
          },
          "citations": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "text": {
            "type": "string"
          }
        }
      },
      "AddParticipantRequest": {
        "type": "object",
        "description": "`POST /v1/events/{id}/participants` request body.",
        "required": [
          "persona",
          "role",
          "source",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "attached": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "evidence_class": {
            "type": [
              "object",
              "null"
            ]
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          },
          "role": {
            "type": "object"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ApproveRequest": {
        "type": "object",
        "description": "Body for approving a proposal. Every field is optional and an absent\nor empty body is valid — approving with no body accepts the proposal\nexactly as it was filed.",
        "properties": {
          "summary_shown": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "AssertionDto": {
        "type": "object",
        "required": [
          "id",
          "persona",
          "source",
          "claim",
          "status",
          "fabricated",
          "evidence",
          "requested_by",
          "kind",
          "created_at_ms"
        ],
        "properties": {
          "claim": {
            "type": "string"
          },
          "created_at_ms": {
            "type": "integer",
            "format": "int64",
            "description": "When the claim was recorded, in milliseconds since the Unix epoch.\nThis is what `sort=created` orders by. It is a wall-clock reading\nmeant for display, and two claims recorded in the same millisecond\nshare a value — do not treat it as a total ordering.",
            "minimum": 0
          },
          "disposition": {
            "type": [
              "string",
              "null"
            ],
            "description": "How a reviewer has so far disposed of this pending item.\n`\"deferred\"` means it was explicitly set aside; omitted means no\ndecision has been recorded yet. Confirmed and refuted items leave\nthe pending queue entirely, so no other value appears here.\nPresent only on `GET /v1/pending`."
          },
          "event_context": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/EventContextDto",
                "description": "The life event this claim is about, joined in so a reviewer can\njudge it without a second request. Present only on\n`GET /v1/pending`, and only for claims whose `kind` is\n`\"event_participation\"`; omitted everywhere else."
              }
            ]
          },
          "evidence": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EvidenceView"
            }
          },
          "fabricated": {
            "type": "boolean"
          },
          "fact_kind": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which kind of fact this is, as a lowercase string (`\"occupation\"`,\n`\"sex\"`, …). Omitted for claims whose `kind` is not `\"fact\"`, and\nfor facts recorded before this field existed. When you need a\nstructured discriminator, use this if it is present and only fall\nback to parsing the `claim` text when it is absent — never the\nother way round."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "description": "What sort of claim this is, as a lowercase string (`\"fact\"`,\n`\"transcription_claim\"`, …). This is what `?kind=` on\n`GET /v1/assertions` and `GET /v1/pending` compares against."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name this assertion states, when it is a name assertion —\npresent only for `kind: \"name\"`, absent for every other kind.\nRender this rather than `claim`: a name assertion's `claim` text\ndescribes how the name is stored and never contains the name\nitself."
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "proposer": {
            "type": [
              "string",
              "null"
            ],
            "description": "The model identifier the creating agent reported for itself, and\nwhat `GET /v1/pending?proposer=` matches on. `null` for a claim\nmade by a human or produced by an import. The value is\nself-declared and never verified — present it as a claim about\norigin, not as identification."
          },
          "requested_by": {
            "type": "string",
            "description": "Who made this claim: `\"agent\"` or `\"human\"`. Only agent-made\nclaims can be marked fabricated."
          },
          "source": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "AssignmentContentDto": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "prompt_name",
              "arguments",
              "kind"
            ],
            "properties": {
              "arguments": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "propertyNames": {
                  "type": "string"
                }
              },
              "kind": {
                "type": "string",
                "enum": [
                  "structured"
                ]
              },
              "prompt_name": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "text",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "raw"
                ]
              },
              "text": {
                "type": "string"
              }
            }
          }
        ],
        "description": "The content of an assignment: structured fields by default, with raw\ntext only when a caller deliberately supplied it instead."
      },
      "AssignmentDto": {
        "type": "object",
        "description": "`GET /v1/assignments/{id}`'s `200` body.",
        "required": [
          "id",
          "content",
          "recorded_at_ms"
        ],
        "properties": {
          "content": {
            "$ref": "#/components/schemas/AssignmentContentDto"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "recorded_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          }
        }
      },
      "AssociationIdDto": {
        "type": "object",
        "required": [
          "assertion_id"
        ],
        "properties": {
          "assertion_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "AttachEvidenceRequest": {
        "type": "object",
        "required": [
          "evidence"
        ],
        "properties": {
          "evidence": {
            "type": "object",
            "description": "The evidence to attach, as a tagged JSON object. The accepted\nforms are `{\"kind\":\"source_blob\",\"blob\":\"<64 hex chars>\"}` and\n`{\"kind\":\"external_record\",\"id\":{\"kind\":\"permalink\",\"url\":\"…\"}}`.\nThe schema is an opaque object; the two shapes above are what the\nroute validates against."
          }
        }
      },
      "AttachSourceBlobRequest": {
        "type": "object",
        "required": [
          "blob"
        ],
        "properties": {
          "blob": {
            "type": "string",
            "description": "The blob's content address as bare hex — 64 characters, no\n`sha256:` prefix. This is the exact string `POST /v1/blobs`\nreturns."
          }
        }
      },
      "AuditChangeDto": {
        "type": "object",
        "description": "One field a recorded event states, and the value it recorded for it.\n\n`field` is a stable snake_case token — `\"kind\"`, `\"date\"`, `\"place\"`,\n`\"title\"`, `\"label\"`, `\"text\"`, `\"claim\"` — safe to branch on.\n`value` is already-formatted, human-readable text, never a raw id.\n\nThis is what an event recorded, not a diff. The log is append-only, so\na correction carries only the value it set; there is no previous value\nto report and none is inferred.",
        "required": [
          "field",
          "value"
        ],
        "properties": {
          "field": {
            "type": "string"
          },
          "value": {
            "type": "string"
          }
        }
      },
      "AuditEntryDto": {
        "type": "object",
        "description": "One entry in the audit trail: who did what, when, and to which\nentity. Durable across restarts.\n\nMostly metadata. `changes` is the one field carrying genealogical\ncontent, for a closed set of fields and only under\n`?include=payload`. `subject` names no entity itself — resolve it\nthrough `GET /v1/labels` when you need a readable name.",
        "required": [
          "id",
          "event_type",
          "actor",
          "at_ms",
          "kind"
        ],
        "properties": {
          "actor": {
            "type": "string"
          },
          "actor_model": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the writing agent called itself. Client-declared and never\nverified — present it as a claim about origin, not as\nidentification. Omitted for every entry not written by an agent."
          },
          "actor_reported_harness": {
            "type": [
              "string",
              "null"
            ],
            "description": "The harness the writing agent declared, on the same footing as\n`actor_model`. Absent when the client declared nothing, and that\nabsence is never filled in with a guess."
          },
          "actor_session": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The sitting this entry was written in. Server-minted, so unlike the\ntwo fields above it identifies rather than claims. Absent for writes\nfrom a surface with no connection."
          },
          "at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "changes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuditChangeDto"
            },
            "description": "What this entry states was recorded — a closed, structured\nalternative to the raw payload. Omitted rather than sent as an\nempty array when there is nothing to state, so presence is the\ntest. An ephemeral `mcp:*` note never carries this, for the same\nreason it carries no subject: its call arguments were hashed at\ncapture and the plaintext never stored.\n\nThis is genealogical content, so it is served only when you pass\n`?include=payload`. A default audit read stays metadata-only."
          },
          "event_type": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "$ref": "#/components/schemas/AuditEntryKindDto",
            "description": "Which producer this row came from — see [`AuditEntryKindDto`].\nAlways present: a row whose kind a reader has to infer is a row a\nreader can miscount."
          },
          "payload": {
            "description": "Extra detail for ephemeral notes only — hashed call arguments and\napproval summaries. Served only when you pass `?include=payload`,\nand never present on a durable entry."
          },
          "subject": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/AuditSubjectDto",
                "description": "The entity this entry changed, when the event names one. Omitted\nrather than null when it does not, so \"is there somewhere to\nnavigate to\" is a presence test.\n\nEphemeral `mcp:*` notes never carry a subject: their call arguments\nwere hashed at capture and the plaintext never stored, so none can\nbe recovered after the fact."
              }
            ]
          },
          "via": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which surface the write arrived over: `\"mcp\"` or `\"http\"`. Set by\nthe service, not by the caller."
          }
        }
      },
      "AuditEntryKindDto": {
        "type": "string",
        "description": "Which of the audit trail's two producers a row came from.\n\nThe trail merges a durable half and an ephemeral one. A `change` is a\ndomain event the engine appended to the tree's log — something about\nthe tree is different because of it. A `session_record` is the\ndaemon's own note that a call happened: an MCP tool call (including\nevery read, which mutates nothing) or the summary text a human was\nshown at approval. It lives in a bounded in-memory ring and is gone at\nrestart.\n\nA caller counting changes counts `Change` rows and no others. The\ndistinction is a type on the wire rather than a prefix a reader is\nexpected to notice, so a consumer cannot add a read to a change count.",
        "enum": [
          "change",
          "session_record"
        ]
      },
      "AuditSubjectDto": {
        "type": "object",
        "description": "What an audit entry was about: an id and a kind.\n\n`kind` is one of `person`, `persona`, `source`, `assertion` or\n`research_question`. There is deliberately **no name here** — an audit\nread carries metadata only. To show a reader a name, resolve these ids\nin one batch with `GET /v1/labels`.",
        "required": [
          "kind",
          "id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string"
          }
        }
      },
      "BackupConfigDto": {
        "type": "object",
        "description": "Where backups are written and how many are kept. Requires the human\ntoken.\n\n`backup_root_path` is a real absolute filesystem path — one of the few\nplaces this API returns one at all, and the reason the route is closed\nto agent-class callers.",
        "required": [
          "enabled",
          "cadence_hours",
          "retention"
        ],
        "properties": {
          "backup_root_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "cadence_hours": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "enabled": {
            "type": "boolean"
          },
          "retention": {
            "$ref": "#/components/schemas/BackupRetentionDto"
          }
        }
      },
      "BackupManifestDto": {
        "type": "object",
        "description": "One restorable backup. Always identified, whether or not it could be\nread: a backup this build cannot interpret is reported as such rather\nthan omitted, because an inventory that silently drops entries looks\ncomplete and is not.",
        "required": [
          "tree_id",
          "backup_id"
        ],
        "properties": {
          "app_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "backup_id": {
            "type": "string"
          },
          "created_at_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "event_count": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "object_count": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "total_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "What a restore of this backup would hash-verify, in bytes.",
            "minimum": 0
          },
          "tree_id": {
            "type": "string"
          },
          "tree_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "`null` when this backup could not be read."
          },
          "unreadable": {
            "type": [
              "string",
              "null"
            ],
            "description": "`null` when readable; otherwise a stable code — `missing`,\n`malformed`, or `unsupported_format_version`."
          }
        }
      },
      "BackupManifestsDto": {
        "type": "object",
        "required": [
          "manifests"
        ],
        "properties": {
          "manifests": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BackupManifestDto"
            }
          }
        }
      },
      "BackupRetentionDto": {
        "type": "object",
        "description": "How many backups to keep before older ones are pruned. Pruning runs\nafter a new backup commits, never before.",
        "required": [
          "daily",
          "weekly",
          "monthly"
        ],
        "properties": {
          "daily": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "monthly": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "weekly": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "BackupStatusDto": {
        "type": "object",
        "description": "Whether backups are configured, and when the last one ran. Readable\nby either actor class, which is why it describes the destination in\nwords rather than returning the path itself — read\n`GET /v1/backup/config` with the human token for that.",
        "required": [
          "enabled",
          "configured",
          "backup_location_description"
        ],
        "properties": {
          "backup_location_description": {
            "type": "string"
          },
          "configured": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean"
          },
          "last_failure_at_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "last_failure_class": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_success_at_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "next_run_at_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          }
        }
      },
      "BlobRefDto": {
        "type": "object",
        "description": "`POST /v1/blobs`'s response: the content address of the bytes you just\nuploaded, in `sha256:<64 hex chars>` form. Pass it verbatim to\n`GET /v1/blobs/{address}` to read them back. Note that routes taking a\nblob *in* a request body — attaching evidence, for instance — want the\nbare 64-character hex without the `sha256:` prefix.",
        "required": [
          "blob"
        ],
        "properties": {
          "blob": {
            "type": "string"
          }
        }
      },
      "BulkConfirmFailureDto": {
        "type": "object",
        "description": "One item `POST /v1/pending/bulk-confirm` could not confirm. `reason`\ncomes from a closed, machine-readable vocabulary — `\"not_found\"`,\n`\"already_confirmed\"`, `\"refuted\"`, `\"superseded\"`,\n`\"not_verifiable\"`, and so on — never a free-text message, so it is\nsafe to branch on.",
        "required": [
          "id",
          "reason"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reason": {
            "type": "string"
          }
        }
      },
      "BulkConfirmRequest": {
        "type": "object",
        "required": [
          "assertion_ids"
        ],
        "properties": {
          "assertion_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "batch_note": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "BulkConfirmResponseDto": {
        "type": "object",
        "description": "`POST /v1/pending/bulk-confirm`'s response: what was confirmed, and\none entry per item that was not. A partly-successful batch is the\nnormal case, not an error — check `failures` even on a `200`.",
        "required": [
          "batch_id",
          "confirmed",
          "failed",
          "duration_ms"
        ],
        "properties": {
          "batch_id": {
            "type": "string",
            "format": "uuid"
          },
          "confirmed": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "duration_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "failed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkConfirmFailureDto"
            }
          }
        }
      },
      "CapabilitiesDto": {
        "type": "object",
        "required": [
          "ingest",
          "api_version",
          "app_version",
          "platform"
        ],
        "properties": {
          "api_version": {
            "type": "string"
          },
          "app_version": {
            "type": "string",
            "description": "The version of the running application, distinct from\n`api_version`, which versions this HTTP contract. A client that\nships alongside the app compares this with its own build on\nreconnect to detect that the service was updated underneath it."
          },
          "ingest": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "platform": {
            "type": "string",
            "description": "This installation's platform key (`darwin-aarch64`,\n`windows-x86_64`, …) — the canonical identifier the update feed is\naddressed by, and the exact value a feedback submission will carry.\nRead it rather than deriving your own, so what you show a user\nbefore sending matches what actually gets sent."
          }
        }
      },
      "CaptureRequest": {
        "type": "object",
        "required": [
          "persona",
          "source",
          "claim",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "claim": {
            "type": "string"
          },
          "fact_kind": {
            "description": "Optional structured discriminator for the fact being captured\n(`\"sex\"`, `\"occupation\"`, …), sent as a JSON value. Omit it to\ncapture a text-only claim; omitting it is always valid."
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid",
            "description": "Required. A capture always records where the claim came from and\nwhy it was made; there is no way to capture a claim without writing\nthe accompanying research-log entry."
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ChecklistViewDto": {
        "type": "object",
        "description": "The five elements of the Genealogical Proof Standard for one research\nquestion, each with its current state — a reasonably exhaustive\nsearch, complete citations, analysis and correlation, resolution of\nconflicts, and a written conclusion.",
        "required": [
          "exhaustive_search",
          "complete_citations",
          "analysis_correlation",
          "conflict_resolution",
          "written_conclusion"
        ],
        "properties": {
          "analysis_correlation": {
            "type": "boolean"
          },
          "complete_citations": {
            "type": "boolean"
          },
          "conflict_resolution": {
            "type": "boolean"
          },
          "exhaustive_search": {
            "$ref": "#/components/schemas/ExhaustiveSearchStatusDto"
          },
          "written_conclusion": {
            "type": "boolean"
          }
        }
      },
      "CitationElementDto": {
        "type": "object",
        "description": "One typed part of a citation, flattened to `kind`, `label` and\n`value` so it can be rendered as a labelled line without interpreting\na nested structure.\n\n`kind` is one of `source_type`, `author`, `title`, `publication`,\n`repository`, `locator`, `access_date`, `identifier`, `free_text` or\n`other`. `label` is set only for `other`, the one kind that carries a\ncaller-chosen label of its own.",
        "required": [
          "kind",
          "value"
        ],
        "properties": {
          "kind": {
            "type": "string"
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "value": {
            "type": "string"
          }
        }
      },
      "CitationRenderRequest": {
        "type": "object",
        "description": "`POST /v1/citations/render` request body: exactly one of `citation_id`\nor `template_id` + `elements` must be set (dispatch-time validated, per\n`event.correct`'s field-discriminated-input precedent).",
        "properties": {
          "citation_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "elements": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "propertyNames": {
              "type": "string"
            }
          },
          "form": {
            "type": [
              "string",
              "null"
            ],
            "description": "`\"first\" | \"subsequent\" | \"bibliography\" | \"all\"` (default `\"all\"`)."
          },
          "template_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CitationRenderResultDto": {
        "type": "object",
        "required": [
          "warnings"
        ],
        "properties": {
          "bibliography": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RenderedTextDto"
              }
            ]
          },
          "csl": {
            "type": [
              "object",
              "null"
            ]
          },
          "first": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RenderedTextDto"
              }
            ]
          },
          "subsequent": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RenderedTextDto"
              }
            ]
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CitationTemplateDto": {
        "type": "object",
        "description": "A citation template: a source type and the fields it asks for. The\ntemplate's own render strings are deliberately not exposed — supply\nelement values and let `POST /v1/citations/render` produce the prose,\nrather than reimplementing the formatting.",
        "required": [
          "id",
          "label",
          "category",
          "fields"
        ],
        "properties": {
          "category": {
            "type": "string"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CitationTemplateFieldDto"
            }
          },
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "CitationTemplateFieldDto": {
        "type": "object",
        "description": "One field of a citation template: what to ask for, and which citation\nelement the answer is stored as. `element` is the snake_case element\nkind (`author`, `title`, `repository`, …).",
        "required": [
          "id",
          "label",
          "required",
          "element"
        ],
        "properties": {
          "element": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "required": {
            "type": "boolean"
          }
        }
      },
      "CitationTemplatesListDto": {
        "type": "object",
        "description": "`GET /v1/citation-templates` response.",
        "required": [
          "registry_version",
          "templates"
        ],
        "properties": {
          "registry_version": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CitationTemplateDto"
            }
          }
        }
      },
      "ClaimTranscriptionRequest": {
        "type": "object",
        "description": "`POST /v1/assertions/transcription-claim`'s request body: a claim\nabout what a document actually says, as distinct from a claim about\nwhat happened.\n\nThe route mints a new assertion rather than annotating an existing\none, which is why it is not addressed by an assertion id. The body\nmirrors an ordinary capture, plus `of_blob` — the content address of\nthe image or file being transcribed.",
        "required": [
          "persona",
          "source",
          "of_blob",
          "claim",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "claim": {
            "type": "string"
          },
          "of_blob": {
            "type": "string",
            "description": "`\"sha256:<64 hex chars>\"` — the same content-address text\n`POST /v1/blobs` returns and `parse_blob_address` parses back."
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "region": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/RegionDto",
                "description": "Where in the image the claim reads from, in normalized fractions\nof the image box. Optional: an unanchored claim is valid; a\nregion is never guessed or back-filled."
              }
            ]
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ClaimTreeRequest": {
        "type": "object",
        "description": "Body for `POST /v1/trees/claim`. `provisional_tree_id` is the tree\nfirst-run onboarding was mounted against; `name` is what the reader\ntyped.",
        "required": [
          "provisional_tree_id",
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "provisional_tree_id": {
            "type": "string"
          }
        }
      },
      "CloseQuestionRequest": {
        "type": "object",
        "description": "Body for closing a research question. Every field is optional and the\nbody itself may be absent or empty. `disposition` and `reason` record\nhow the lead was retired; both are carried into later session briefs,\nso a closure with a reason stays explainable later.",
        "properties": {
          "disposition": {
            "type": [
              "string",
              "null"
            ],
            "description": "`answered | abandoned | superseded | merged`."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ConfirmedAssertionBriefDto": {
        "type": "object",
        "required": [
          "id",
          "claim"
        ],
        "properties": {
          "claim": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ConfirmedAssertionsSectionDto": {
        "type": "object",
        "required": [
          "total",
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConfirmedAssertionBriefDto"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "ConflictClaimDto": {
        "type": "object",
        "description": "One side of a conflict: the claim, and the source standing behind it.\n\n`fact_kind` is the machine name of the disputed field (`\"occupation\"`,\n`\"sex\"`, …) when the claim is a fact, and `null` otherwise; presenting\nit as a label is the caller's job, so no user-facing wording is minted\nhere.",
        "required": [
          "assertion",
          "persona",
          "claim",
          "kind",
          "status",
          "fabricated",
          "source",
          "has_evidence",
          "is_confirmable"
        ],
        "properties": {
          "assertion": {
            "type": "string",
            "format": "uuid"
          },
          "claim": {
            "type": "string"
          },
          "event": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ConflictClaimEventDto",
                "description": "The life event this claim is about, when the claim is a\nparticipation in one — resolved here so you can describe the claim\nwithout a second call.\n\nUse it rather than `claim` for anything a person will read. A\nparticipation's `claim` text names its event by id and says nothing\nelse, so two claims about *different* events read as the same\nsentence, and a conflict between them cannot be told apart.\nOmitted for every claim that is not a participation, and for one\nwhose event is no longer present."
              }
            ]
          },
          "fabricated": {
            "type": "boolean"
          },
          "fact_kind": {
            "type": [
              "string",
              "null"
            ]
          },
          "has_evidence": {
            "type": "boolean",
            "description": "Whether any record stands behind this claim's assertion — including\none only the reader holds. Says the claim is sourced, never that it\nis ready to confirm; `is_confirmable` says that."
          },
          "is_confirmable": {
            "type": "boolean",
            "description": "Whether `POST /v1/assertions/{id}/confirm` would be accepted for\nthis claim's assertion, so a surface can offer \"confirm what is\nattached\" rather than a button the record will refuse. False\nwhenever every attached record is one only the reader holds: that\nsources the claim and cannot settle it."
          },
          "kind": {
            "type": "string"
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          },
          "source_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "The source's own title, joined in so the claim can name where it\ncame from. `null` when the source is no longer readable."
          },
          "status": {
            "type": "string",
            "description": "Whether this claim may yet feed the proof argument that settles\nthe dispute. Only a confirmed, unrefuted, unfabricated claim can,\nso a resolution surface reads this rather than offering a\nconclusion the record will refuse."
          }
        }
      },
      "ConflictClaimEventDto": {
        "type": "object",
        "description": "The life event behind one side of a conflict, and the role that side\nputs its person in — enough to describe what the claim is about\nwithout a second call.\n\nCarries the event's own recorded detail only. Who else took part in it\nis not included; fetch the event itself if you need that.",
        "required": [
          "event_id",
          "kind",
          "role"
        ],
        "properties": {
          "date": {
            "type": [
              "object",
              "null"
            ]
          },
          "date_phrase": {
            "type": [
              "string",
              "null"
            ]
          },
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "object"
          },
          "kind_phrase": {
            "type": [
              "string",
              "null"
            ]
          },
          "place": {
            "type": [
              "object",
              "null"
            ]
          },
          "role": {
            "type": "object",
            "description": "What part this side's person played in the event (\"principal\",\n\"witness\", \"godparent\", …), in the same tagged form event\nparticipations use everywhere else."
          }
        }
      },
      "ConflictDetectionBriefDto": {
        "type": "object",
        "description": "The state of conflict detection, not just a list of conflicts.\nWhether detection has run is reported separately from what it found,\nbecause \"no conflicts found\" and \"never looked\" are different claims\nand an empty list cannot tell them apart.",
        "required": [
          "trigger",
          "detection_has_run",
          "imports_observed",
          "conflicts_recorded",
          "open_conflicts"
        ],
        "properties": {
          "conflicts_recorded": {
            "type": "integer",
            "minimum": 0
          },
          "detection_has_run": {
            "type": "boolean"
          },
          "imports_observed": {
            "type": "integer",
            "minimum": 0
          },
          "open_conflicts": {
            "type": "integer",
            "minimum": 0
          },
          "trigger": {
            "type": "string"
          }
        }
      },
      "ConflictDto": {
        "type": "object",
        "description": "One detected conflict: whose it is, what is in dispute, and whether it\nis still open.\n\n`claims` carries each disagreeing claim already joined to the source\nthat made it, so a reader can name the dispute without a second\nrequest — the same \"join it in so the reviewer can judge it here\"\nposture `GET /v1/pending`'s `event_context` takes. `assertions` is the\nbare id list, kept for callers that only need identity.",
        "required": [
          "id",
          "scope",
          "assertions",
          "open",
          "claims"
        ],
        "properties": {
          "assertions": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "claims": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConflictClaimDto"
            },
            "description": "What each side actually claims. Empty only if the conflict names\nassertions the read model no longer holds."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "open": {
            "type": "boolean"
          },
          "scope": {
            "$ref": "#/components/schemas/ConflictScopeDto"
          }
        }
      },
      "ConflictScopeDto": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "persona",
              "layer"
            ],
            "properties": {
              "layer": {
                "type": "string",
                "enum": [
                  "persona"
                ]
              },
              "persona": {
                "type": "string",
                "format": "uuid"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "person",
              "layer"
            ],
            "properties": {
              "layer": {
                "type": "string",
                "enum": [
                  "identity"
                ]
              },
              "person": {
                "type": "string",
                "format": "uuid"
              }
            }
          }
        ],
        "description": "Which layer a conflict lives at, tagged by `layer`: two claims about\none persona disagree (`persona`), or the evidence disagrees about\nwhether personas are the same human (`identity`). A conflict always\nbelongs to exactly one persona or one person — a reader filtering to\nwhat it is showing uses this, never the assertion ids."
      },
      "ConnectClaudeDesktopResultDto": {
        "type": "object",
        "description": "The result of connecting an MCP client to this installation: which\nconfig file was written, and — in words rather than as a path — where\nthe previous version of it was backed up.",
        "required": [
          "action",
          "backup_path_description",
          "restart_required"
        ],
        "properties": {
          "action": {
            "type": "string",
            "description": "`\"created\"` (no prior config file existed) or `\"merged\"` (an\nexisting config was backed up then merged into)."
          },
          "backup_path_description": {
            "type": "string"
          },
          "restart_required": {
            "type": "boolean"
          }
        }
      },
      "ContraryTreatmentView": {
        "type": "object",
        "description": "How a piece of contrary evidence was dealt with in a proof argument:\nwhat the conflicting evidence was, and the reasoning that resolved or\nset it aside.",
        "required": [
          "assertion",
          "treatment"
        ],
        "properties": {
          "assertion": {
            "type": "string",
            "format": "uuid"
          },
          "treatment": {
            "type": "string"
          }
        }
      },
      "CorrectEventRequest": {
        "type": "object",
        "description": "`POST /v1/events/{id}/corrections`'s request body. One route corrects\nany of three things; `field` says which: `\"date\"`, `\"place\"` or\n`\"kind\"`. Supply the matching value field — `kind_phrase` is read only\nwhen `field` is `\"kind\"`.",
        "required": [
          "field",
          "value",
          "source_id"
        ],
        "properties": {
          "evidence": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "field": {
            "type": "string"
          },
          "kind_phrase": {
            "type": [
              "string",
              "null"
            ]
          },
          "source_id": {
            "type": "string",
            "format": "uuid"
          },
          "value": {
            "type": "object"
          }
        }
      },
      "CorrectionResultDto": {
        "type": "object",
        "description": "The outcome of a correction, which depends on who asked. A human-token\nrequest applies it directly and returns `200`. An agent-token request\nfiles a proposal instead and returns `202` with a `proposal_id` for a\nhuman to approve. The body has the same shape either way, so check the\nstatus code or the presence of `proposal_id`.",
        "required": [
          "applied"
        ],
        "properties": {
          "applied": {
            "type": "boolean"
          },
          "proposal_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "CreateCitationRequest": {
        "type": "object",
        "description": "`POST /v1/citations`'s request body: an arbitrary list of typed\ncitation elements, for a citation that no template covers. When a\ntemplate does fit, prefer `POST /v1/sources/from-template`, which\nkeys the same elements by template field id. Each element is sent as\nan opaque JSON value whose shape this schema does not enumerate.",
        "required": [
          "elements"
        ],
        "properties": {
          "elements": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "CreateCitationResultDto": {
        "type": "object",
        "description": "`POST /v1/citations`'s response — the new citation's id.",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "CreatePersonRequest": {
        "type": "object",
        "description": "`POST /v1/persons`'s request body: conclude that a set of personas are\nthe same human being. `certainty` is sent as an opaque JSON value\nwhose shape this schema does not enumerate.",
        "required": [
          "personas",
          "certainty"
        ],
        "properties": {
          "certainty": {
            "type": "object"
          },
          "personas": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "proof_argument": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "CreatePersonResultDto": {
        "type": "object",
        "description": "The outcome of concluding a person. A human-token request applies it\nand returns `200` with the new person's id; an agent-token request\nfiles a proposal and returns `202` with a `proposal_id` for a human to\napprove.",
        "required": [
          "applied"
        ],
        "properties": {
          "applied": {
            "type": "boolean"
          },
          "id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proposal_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "CreatePersonaRequest": {
        "type": "object",
        "required": [
          "source",
          "label"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "CreateSourceRequest": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string"
          }
        }
      },
      "CreateTreeRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          }
        }
      },
      "DenyRequest": {
        "type": "object",
        "required": [
          "reason"
        ],
        "properties": {
          "reason": {
            "type": "string"
          }
        }
      },
      "DiagnosticsDto": {
        "type": "object",
        "properties": {
          "log_location_description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where this installation's log file is, phrased as a sentence a\nhuman can act on (\"daily-rotating log file under …\").\n\nThe sentence **does contain a real absolute path**. The\n`_description` suffix means it is prose rather than a bare path\nvalue; it does not mean the path has been removed. Because of\nthat, this field is **human-token only** and is `null` for an\nagent-class caller."
          }
        }
      },
      "DuplicateCandidateDto": {
        "type": "object",
        "description": "One ranked hit from `GET /v1/persons/duplicate-candidates`: an\nexisting person who may be the same as the one being entered. The\n`score` and its band are computed server-side and carried verbatim —\ndo not re-derive or re-threshold them client-side, or two callers will\ndisagree about the same pair.",
        "required": [
          "id",
          "name",
          "score_permille",
          "likelihood"
        ],
        "properties": {
          "birth_year": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "A representative birth year, not a full date — enough to\ndisambiguate a \"did you mean?\" line. Read the person itself for the\ndated evidence."
          },
          "death_year": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "likelihood": {
            "type": "string",
            "description": "`likely` or `possible` — `unlikely` rows are never sent."
          },
          "name": {
            "type": "string",
            "description": "The candidate's display name, derived the same way a person's\n`name` is."
          },
          "score_permille": {
            "type": "integer",
            "format": "int32",
            "description": "`[0, 1000]` permille on the domain scale (`SimilarityScore`).",
            "minimum": 0
          }
        }
      },
      "EditDispositionDto": {
        "type": "object",
        "description": "The most recent human edit, and whether it can still be undone.",
        "required": [
          "event",
          "event_type",
          "undoable"
        ],
        "properties": {
          "event": {
            "type": "string",
            "format": "uuid"
          },
          "event_type": {
            "type": "string"
          },
          "undoable": {
            "$ref": "#/components/schemas/UndoableNowDto"
          }
        }
      },
      "EntityLabelDto": {
        "type": "object",
        "description": "One resolved label: what a human calls the entity with this id.\n\n`kind` reports what the id turned out to be; you do not supply it, and\nids are unique across kinds, so there is nothing to guess.",
        "required": [
          "id",
          "kind",
          "label"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "EventContextDto": {
        "type": "object",
        "required": [
          "event_id",
          "kind",
          "source_id",
          "sibling_participations"
        ],
        "properties": {
          "date": {
            "type": [
              "object",
              "null"
            ]
          },
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "object"
          },
          "kind_phrase": {
            "type": [
              "string",
              "null"
            ]
          },
          "place": {
            "type": [
              "object",
              "null"
            ]
          },
          "sibling_participations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiblingParticipationDto"
            }
          },
          "source_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "EvidenceView": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "content_address",
              "kind"
            ],
            "properties": {
              "content_address": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "source_blob"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "record",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "external_record"
                ]
              },
              "record": {
                "$ref": "#/components/schemas/ExternalRecordIdView"
              }
            }
          },
          {
            "type": "object",
            "description": "A record the reader holds and no client can fetch. It sources the\nclaim; it never satisfies `POST /v1/assertions/{id}/confirm`.",
            "required": [
              "citation",
              "kind"
            ],
            "properties": {
              "citation": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "offline_record"
                ]
              }
            }
          }
        ],
        "description": "A record standing behind a claim — a stored blob identified by its\ncontent address, a record held elsewhere that a client can open, or a\nrecord only the reader holds. Never a path on the machine running the\nservice: a blob is always referenced by content address, which any\nclient can fetch through `GET /v1/blobs/{address}`."
      },
      "ExhaustiveSearchStatusDto": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "not_started"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "covered",
              "total",
              "status"
            ],
            "properties": {
              "covered": {
                "type": "integer",
                "format": "int32",
                "minimum": 0
              },
              "status": {
                "type": "string",
                "enum": [
                  "in_progress"
                ]
              },
              "total": {
                "type": "integer",
                "format": "int32",
                "minimum": 0
              }
            }
          },
          {
            "type": "object",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "claimable"
                ]
              }
            }
          }
        ],
        "description": "How far the search behind a conclusion has actually gone — the first\nelement of the Genealogical Proof Standard. Reported as a state, never\nasserted as \"complete\" on the strength of an absent record."
      },
      "ExportCitationDto": {
        "type": "object",
        "required": [
          "assertion_id",
          "citation_id",
          "rendered_footnote"
        ],
        "properties": {
          "assertion_id": {
            "type": "string",
            "format": "uuid"
          },
          "citation_id": {
            "type": "string",
            "format": "uuid"
          },
          "rendered_footnote": {
            "type": "string"
          }
        }
      },
      "ExportProvenanceDto": {
        "type": "object",
        "required": [
          "creator",
          "at_ms"
        ],
        "properties": {
          "at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "creator": {
            "type": "string"
          },
          "via": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ExportQuestionDto": {
        "type": "object",
        "required": [
          "id",
          "text"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "text": {
            "type": "string"
          }
        }
      },
      "ExportSectionDto": {
        "type": "object",
        "required": [
          "kind",
          "body",
          "citations"
        ],
        "properties": {
          "body": {
            "type": "string"
          },
          "citations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExportCitationDto"
            }
          },
          "kind": {
            "type": "string"
          }
        }
      },
      "ExternalRecordIdView": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "ark",
              "kind"
            ],
            "properties": {
              "ark": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "family_search_ark"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "dbid",
              "record",
              "kind"
            ],
            "properties": {
              "dbid": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "ancestry_record"
                ]
              },
              "record": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "doi",
              "kind"
            ],
            "properties": {
              "doi": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "doi"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "url",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "permalink"
                ]
              },
              "url": {
                "type": "string"
              }
            }
          }
        ],
        "description": "How a record held outside this app is identified, tagged by `kind`: a\nFamilySearch ark (`family_search_ark`), an Ancestry database and\nrecord pair (`ancestry_record`), a DOI (`doi`), or a plain stable URL\n(`permalink`). Prefer a specific form over `permalink` when one fits —\nit survives a site reorganising its URLs."
      },
      "FanEdgeDto": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "assertion": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "event": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "own_event": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "own_role": {
            "type": [
              "string",
              "null"
            ]
          },
          "place_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "relation": {
            "type": [
              "object",
              "null"
            ]
          },
          "their_event": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "their_role": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": "string"
          }
        }
      },
      "FanNeighborDto": {
        "type": "object",
        "required": [
          "persona",
          "edges"
        ],
        "properties": {
          "edges": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FanEdgeDto"
            }
          },
          "person": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "FanNetworkDto": {
        "type": "object",
        "required": [
          "neighbors"
        ],
        "properties": {
          "neighbors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FanNeighborDto"
            }
          }
        }
      },
      "FeedbackAcceptedDto": {
        "type": "object",
        "description": "What comes back when feedback is delivered: the app version and\nplatform actually attached to the submission, so a client can show the\nuser that what was sent matches what it disclosed beforehand.",
        "required": [
          "app_version",
          "platform"
        ],
        "properties": {
          "app_version": {
            "type": "string"
          },
          "platform": {
            "type": "string"
          }
        }
      },
      "GedcomExportRequest": {
        "type": "object",
        "description": "`POST /v1/gedcom/export`'s request body. `destination` is optional and\nhuman-token-only: it asks the service to *also* write the file to that\npath on this machine. A client that has no filesystem access here\nleaves it unset and fetches the export through the `blob` address in\nthe response instead.",
        "required": [
          "gedcom_version"
        ],
        "properties": {
          "destination": {
            "type": [
              "string",
              "null"
            ]
          },
          "gedcom_version": {
            "type": "string"
          },
          "include_living": {
            "type": "boolean"
          },
          "overwrite": {
            "type": "boolean",
            "description": "Replace the file already at `destination`. Defaults to false, so\na request that does not say this refuses the collision rather\nthan destroying what is there. Never a stored preference on\neither side of the wire: it means \"the reader was shown this\nexact path and said yes to it\", which is only true of the one\nrequest carrying it."
          }
        }
      },
      "GedcomExportResultDto": {
        "type": "object",
        "description": "The outcome of a GEDCOM export, which depends on who asked.\n\nA human-token request exports immediately: `applied` is `true` and\n`blob`, `sha256` and `loss_report_id` are populated. An agent-token\nrequest never exports directly — it files a proposal for a human to\napprove, returning `applied: false` and a `proposal_id`. Check\n`applied` before assuming a file exists.",
        "required": [
          "applied",
          "message"
        ],
        "properties": {
          "applied": {
            "type": "boolean"
          },
          "blob": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exported file's content address in `sha256:<64 hex chars>`\nform, present whenever `applied` is `true` — whether or not a\n`destination` was asked for. Pass it verbatim to\n`GET /v1/blobs/{address}` to fetch the bytes."
          },
          "file": {
            "type": [
              "string",
              "null"
            ],
            "description": "The local path the export was also written to, present only when\nthe request supplied a `destination`. `null` otherwise — use `blob`\nto retrieve the export in that case."
          },
          "loss_report_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "message": {
            "type": "string"
          },
          "proposal_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "sha256": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "HarnessConnectPreviewDto": {
        "type": "object",
        "description": "A dry run of connecting an MCP client: exactly what the real connect\nwould change, computed by the same logic, with nothing written.\n\nOnly the *names* of existing top-level config keys are reported, never\ntheir values — another tool's entry may itself contain paths or\nsecrets that are none of this API's business to echo back.",
        "required": [
          "harness_id",
          "display_name",
          "action",
          "servers_key",
          "existing_keys_preserved",
          "server_entry_already_existed",
          "summary"
        ],
        "properties": {
          "action": {
            "type": "string",
            "description": "`\"create\"` (no prior config file exists) or `\"merge\"` (an existing\nconfig would be backed up then merged into)."
          },
          "display_name": {
            "type": "string"
          },
          "existing_keys_preserved": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Top-level key names (never values) of the existing config that\nwould be left untouched; empty for `action: \"create\"`."
          },
          "harness_id": {
            "type": "string"
          },
          "server_entry_already_existed": {
            "type": "boolean",
            "description": "Whether a `heartwood` entry already exists under `servers_key` and\nwould be overwritten (stale-path repair) rather than newly added."
          },
          "servers_key": {
            "type": "string"
          },
          "summary": {
            "type": "string",
            "description": "A human-readable sentence describing what the write would do."
          }
        }
      },
      "HarnessListItemDto": {
        "type": "object",
        "description": "One MCP client this installation knows how to connect to — the list a\nconnect picker is built from.",
        "required": [
          "id",
          "display_name",
          "vendor",
          "one_click"
        ],
        "properties": {
          "display_name": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "one_click": {
            "type": "boolean",
            "description": "Whether this client can be connected automatically. When `false`,\nthe connect routes will not write its config and the user has to\nconfigure it by hand."
          },
          "vendor": {
            "type": "string"
          }
        }
      },
      "HarnessListResponseDto": {
        "type": "object",
        "required": [
          "harnesses"
        ],
        "properties": {
          "harnesses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HarnessListItemDto"
            }
          }
        }
      },
      "HomePersonDto": {
        "type": "object",
        "description": "A tree's home person: the default anchor a pedigree starts from. The\nsame shape serves the GET response and the PUT body; sending `null`\nclears it. This is a view preference, not genealogical knowledge —\nsetting it records nothing about the person and appears in no audit\ntrail.",
        "properties": {
          "home_person": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "ImportContentsDto": {
        "type": "object",
        "description": "What an import run has left in the tree right now, per entity kind.\n`total` is the sum of every kind, including `other` — it can only read\nzero when undoing the run would remove nothing.",
        "required": [
          "personas",
          "assertions",
          "sources",
          "life_events",
          "notes",
          "citations",
          "media",
          "other",
          "total"
        ],
        "properties": {
          "assertions": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "citations": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "life_events": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "media": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "notes": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "other": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "personas": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "sources": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "total": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "ImportRecordDto": {
        "type": "object",
        "description": "One row of `GET /v1/imports`: a completed import, what it brought in,\nand the content address of the file it came from — never a filesystem\npath, so the original bytes stay fetchable through\n`GET /v1/blobs/{address}`.\n\n`import_key` is the opaque string (`\"via:at_ms\"`) that identifies this\nimport to `POST /v1/imports/{import_key}/undo`. The same string\nappears in a `duplicate-import` problem's extensions, so the two can\nbe matched without parsing either.\n\n`personas`/`assertions` are the historical figures frozen on the run's\nsealing marker; `in_tree` is what the run still owns *now*. They differ\nwhenever the run was cancelled, undone, or partly edited away, and\n`in_tree` is the one a reader means when they ask what a file put in\ntheir tree.",
        "required": [
          "raw_file",
          "import_key",
          "personas",
          "assertions",
          "at_ms",
          "status",
          "undone",
          "in_tree"
        ],
        "properties": {
          "assertions": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "file_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "import_key": {
            "type": "string"
          },
          "in_tree": {
            "$ref": "#/components/schemas/ImportContentsDto"
          },
          "loss_report_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The run's loss report, if one was persisted — what the file could\nNOT bring over, which the counts above never state. Fetch it from\n`GET /gedcom/loss-reports?report_id=`. `null` for a run that\npredates loss reports or whose report file is gone."
          },
          "personas": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "raw_file": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "example": "completed"
          },
          "undone": {
            "type": "boolean"
          }
        }
      },
      "JobDto": {
        "type": "object",
        "description": "One background job — what `GET /v1/jobs/{id}` returns and what\n`GET /v1/jobs` lists.\n\n`status` is one of `queued`, `running`, `progress`, `done`, `failed`\nor `cancelled`. While `progress`, `percent` and `summary` describe how\nfar it has got; on `done`, `summary` describes the result; on `failed`\nor `cancelled`, `reason` says why. Poll until `status` is `done`,\n`failed` or `cancelled` — those three are terminal.\n\nProgress is reported as counts and named phases only. A job never\nechoes back the content it is processing.\n\n`tree_id` is the tree the work is being done to. A tree only ever\nlists and answers for its own jobs, so this is the tree that was asked\n— never a way to discover what is happening in another one.",
        "required": [
          "id",
          "tree_id",
          "kind",
          "status",
          "created_at_ms",
          "updated_at_ms"
        ],
        "properties": {
          "created_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string"
          },
          "percent": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "example": "queued"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "tree_id": {
            "type": "string"
          },
          "updated_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          }
        }
      },
      "LifeEventDto": {
        "type": "object",
        "description": "`GET /v1/events/{id}` response: the anchor plus its live+non-live\nparticipations (each carrying its own status).",
        "required": [
          "id",
          "kind",
          "source",
          "source_title",
          "participations"
        ],
        "properties": {
          "date": {
            "type": [
              "object",
              "null"
            ]
          },
          "date_phrase": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "object"
          },
          "kind_phrase": {
            "type": [
              "string",
              "null"
            ]
          },
          "participations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParticipantViewDto"
            }
          },
          "place": {
            "type": [
              "object",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "format": "uuid"
          },
          "source_title": {
            "type": "string",
            "description": "The title of `source`, carried beside the id so a reader never\nneeds a second call to name the record an event came from."
          }
        }
      },
      "LogDetailAppendRequest": {
        "type": "object",
        "description": "`POST /v1/research/log/{id}/details` request body —\n`research.log.detail.append`'s route.",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "string"
          }
        }
      },
      "LogDetailAppendResponseDto": {
        "type": "object",
        "required": [
          "log_entry_id"
        ],
        "properties": {
          "log_entry_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "LogDetailDto": {
        "type": "object",
        "description": "One detail appended to a research-log entry after it was first\nrecorded — a follow-up note, with its own attestation and timestamp.",
        "required": [
          "id",
          "detail",
          "recorded_at",
          "attestation"
        ],
        "properties": {
          "attestation": {
            "type": "string",
            "description": "Where this text came from: `user_entered`, `agent_reported` or\n`import_derived`. A closed vocabulary, safe to branch on."
          },
          "detail": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "recorded_at": {
            "type": "integer",
            "format": "int64",
            "description": "Milliseconds since epoch."
          }
        }
      },
      "LogEntryTextSetRequest": {
        "type": "object",
        "description": "`PUT /v1/research/log/{id}/text`'s request body. Replaces a log\nentry's search-context text wholesale — a correction, not an append.\nTo add to an entry without rewriting it, append a detail instead.",
        "required": [
          "search_context"
        ],
        "properties": {
          "search_context": {
            "type": "string"
          }
        }
      },
      "LogNegativeRecordRequest": {
        "type": "object",
        "description": "`POST /v1/research/log/negative-evidence` request body —\n`research.log.negative.record`'s route.",
        "required": [
          "log_entry",
          "persona",
          "expectation",
          "inference"
        ],
        "properties": {
          "expectation": {
            "type": "string"
          },
          "inference": {
            "type": "string"
          },
          "log_entry": {
            "type": "string",
            "format": "uuid"
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "LogNegativeRecordResponseDto": {
        "type": "object",
        "required": [
          "negative_evidence_id"
        ],
        "properties": {
          "negative_evidence_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "LossEntryDto": {
        "type": "object",
        "required": [
          "category",
          "code",
          "scope",
          "tag_path",
          "count",
          "detail"
        ],
        "properties": {
          "category": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "detail": {
            "type": "string"
          },
          "record_refs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "scope": {
            "type": "string"
          },
          "tag_path": {
            "type": "string",
            "description": "Where in the GEDCOM tag hierarchy the loss happened, e.g.\n`SNOTE.MIME`. This is a tag path, not a filesystem path."
          }
        }
      },
      "LossReportDto": {
        "type": "object",
        "required": [
          "schema_version",
          "report_id",
          "direction",
          "gedcom_version",
          "created_at_ms",
          "file",
          "totals",
          "entries"
        ],
        "properties": {
          "created_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "direction": {
            "type": "string"
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LossEntryDto"
            }
          },
          "file": {
            "$ref": "#/components/schemas/LossReportFileInfoDto"
          },
          "gedcom_version": {
            "type": "string"
          },
          "report_id": {
            "type": "string",
            "format": "uuid"
          },
          "schema_version": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "totals": {
            "$ref": "#/components/schemas/LossReportTotalsDto"
          }
        }
      },
      "LossReportFileInfoDto": {
        "type": "object",
        "required": [
          "name",
          "sha256",
          "completeness"
        ],
        "properties": {
          "blob": {
            "type": [
              "string",
              "null"
            ]
          },
          "completeness": {
            "type": "string",
            "description": "Whether this is a whole GEDCOM file: `complete`, `no_trailer`,\n`no_header`, `empty`, or `unknown` for a report stored before this\nwas checked.\n\nAnything but `complete` means the file ends before it should, so\nno count in this report describes the whole file — and\n`totals.lossless` is `false` for exactly that reason."
          },
          "name": {
            "type": "string"
          },
          "sha256": {
            "type": "string"
          }
        }
      },
      "LossReportTotalsDto": {
        "type": "object",
        "required": [
          "entry_count",
          "lossless"
        ],
        "properties": {
          "entry_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "lossless": {
            "type": "boolean"
          },
          "records_dropped": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "records_mapped": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "records_seen": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "structures_emitted": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "structures_visited": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "MediaResolveResultDto": {
        "type": "object",
        "description": "The outcome of supplying bytes for one media file.\n\n`status` is `resolved` when this call recorded the bytes, or\n`already_resolved` when the file already read from these exact bytes.\nBoth are successes, and `blob` is the address the caller supplied\neither way — supplying a *different* address corrects the attachment\nrather than being discarded, so a `200` here always means the file now\nreads from what was just uploaded.",
        "required": [
          "status",
          "blob",
          "byte_len"
        ],
        "properties": {
          "blob": {
            "type": "string",
            "description": "The content address this media file now reads from."
          },
          "byte_len": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "status": {
            "type": "string",
            "description": "`resolved` | `already_resolved`."
          }
        }
      },
      "MergePersonsRequest": {
        "type": "object",
        "description": "`POST /v1/persons/merge`'s request.",
        "required": [
          "left",
          "right"
        ],
        "properties": {
          "left": {
            "type": "string",
            "format": "uuid"
          },
          "right": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "MergePersonsResultDto": {
        "type": "object",
        "description": "`POST /v1/persons/merge`'s response — the merged person's id.",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "NegativeFindingDto": {
        "type": "object",
        "description": "A searched-and-found-absent record: formalized negative evidence with\nthe search that evidences it.",
        "required": [
          "log_entry",
          "expectation",
          "inference",
          "recorded_by",
          "recorded_at_ms"
        ],
        "properties": {
          "expectation": {
            "type": "string"
          },
          "inference": {
            "type": "string"
          },
          "log_entry": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "recorded_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "recorded_by": {
            "type": "string"
          },
          "search_context": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "NoteAnchorDto": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "persona"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "person"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "source"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "assertion"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "life_event"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "citation"
                ]
              }
            }
          }
        ],
        "description": "What a note is attached to, as `{kind, id}` — a `person`, `persona`,\n`source`, `assertion`, `life_event` or `citation`."
      },
      "NoteDto": {
        "type": "object",
        "required": [
          "id",
          "text",
          "citations",
          "requested_by"
        ],
        "properties": {
          "citations": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "requested_by": {
            "type": "string"
          },
          "text": {
            "type": "string"
          }
        }
      },
      "NoteIdDto": {
        "type": "object",
        "required": [
          "note_id"
        ],
        "properties": {
          "note_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "OkResponseDto": {
        "type": "object",
        "description": "Shared `{ok: true}` response for the plan-item verbs whose verb table\nentry is just `{ok}` (`reorder`/`remove`).",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          }
        }
      },
      "OpenQuestionBriefDto": {
        "type": "object",
        "description": "An open research question in summary form: counts and a one-line\ndescription, meant as a pointer. Fetch\n`GET /v1/research/questions/{id}` for the question itself.",
        "required": [
          "id",
          "question",
          "subjects",
          "plan_items",
          "log_entries",
          "gps"
        ],
        "properties": {
          "gps": {
            "$ref": "#/components/schemas/ChecklistViewDto"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "log_entries": {
            "type": "integer",
            "minimum": 0
          },
          "plan_items": {
            "type": "integer",
            "minimum": 0
          },
          "question": {
            "type": "string"
          },
          "subjects": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "OpenQuestionsSectionDto": {
        "type": "object",
        "required": [
          "total",
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OpenQuestionBriefDto"
            }
          },
          "total": {
            "type": "integer",
            "description": "Uncapped count; `items` is capped, so `total > items.len()` means\n\"there are more — list them with the research verbs\".",
            "minimum": 0
          }
        }
      },
      "OpenResearchQuestionRequest": {
        "type": "object",
        "required": [
          "question"
        ],
        "properties": {
          "question": {
            "type": "string"
          }
        }
      },
      "Page_AssertionDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "persona",
                "source",
                "claim",
                "status",
                "fabricated",
                "evidence",
                "requested_by",
                "kind",
                "created_at_ms"
              ],
              "properties": {
                "claim": {
                  "type": "string"
                },
                "created_at_ms": {
                  "type": "integer",
                  "format": "int64",
                  "description": "When the claim was recorded, in milliseconds since the Unix epoch.\nThis is what `sort=created` orders by. It is a wall-clock reading\nmeant for display, and two claims recorded in the same millisecond\nshare a value — do not treat it as a total ordering.",
                  "minimum": 0
                },
                "disposition": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "How a reviewer has so far disposed of this pending item.\n`\"deferred\"` means it was explicitly set aside; omitted means no\ndecision has been recorded yet. Confirmed and refuted items leave\nthe pending queue entirely, so no other value appears here.\nPresent only on `GET /v1/pending`."
                },
                "event_context": {
                  "oneOf": [
                    {
                      "type": "null"
                    },
                    {
                      "$ref": "#/components/schemas/EventContextDto",
                      "description": "The life event this claim is about, joined in so a reviewer can\njudge it without a second request. Present only on\n`GET /v1/pending`, and only for claims whose `kind` is\n`\"event_participation\"`; omitted everywhere else."
                    }
                  ]
                },
                "evidence": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EvidenceView"
                  }
                },
                "fabricated": {
                  "type": "boolean"
                },
                "fact_kind": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Which kind of fact this is, as a lowercase string (`\"occupation\"`,\n`\"sex\"`, …). Omitted for claims whose `kind` is not `\"fact\"`, and\nfor facts recorded before this field existed. When you need a\nstructured discriminator, use this if it is present and only fall\nback to parsing the `claim` text when it is absent — never the\nother way round."
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "kind": {
                  "type": "string",
                  "description": "What sort of claim this is, as a lowercase string (`\"fact\"`,\n`\"transcription_claim\"`, …). This is what `?kind=` on\n`GET /v1/assertions` and `GET /v1/pending` compares against."
                },
                "name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The name this assertion states, when it is a name assertion —\npresent only for `kind: \"name\"`, absent for every other kind.\nRender this rather than `claim`: a name assertion's `claim` text\ndescribes how the name is stored and never contains the name\nitself."
                },
                "persona": {
                  "type": "string",
                  "format": "uuid"
                },
                "proposer": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The model identifier the creating agent reported for itself, and\nwhat `GET /v1/pending?proposer=` matches on. `null` for a claim\nmade by a human or produced by an import. The value is\nself-declared and never verified — present it as a claim about\norigin, not as identification."
                },
                "requested_by": {
                  "type": "string",
                  "description": "Who made this claim: `\"agent\"` or `\"human\"`. Only agent-made\nclaims can be marked fabricated."
                },
                "source": {
                  "type": "string",
                  "format": "uuid"
                },
                "status": {
                  "type": "string"
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_AuditEntryDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One entry in the audit trail: who did what, when, and to which\nentity. Durable across restarts.\n\nMostly metadata. `changes` is the one field carrying genealogical\ncontent, for a closed set of fields and only under\n`?include=payload`. `subject` names no entity itself — resolve it\nthrough `GET /v1/labels` when you need a readable name.",
              "required": [
                "id",
                "event_type",
                "actor",
                "at_ms",
                "kind"
              ],
              "properties": {
                "actor": {
                  "type": "string"
                },
                "actor_model": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "What the writing agent called itself. Client-declared and never\nverified — present it as a claim about origin, not as\nidentification. Omitted for every entry not written by an agent."
                },
                "actor_reported_harness": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The harness the writing agent declared, on the same footing as\n`actor_model`. Absent when the client declared nothing, and that\nabsence is never filled in with a guess."
                },
                "actor_session": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid",
                  "description": "The sitting this entry was written in. Server-minted, so unlike the\ntwo fields above it identifies rather than claims. Absent for writes\nfrom a surface with no connection."
                },
                "at_ms": {
                  "type": "integer",
                  "format": "int64",
                  "minimum": 0
                },
                "changes": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AuditChangeDto"
                  },
                  "description": "What this entry states was recorded — a closed, structured\nalternative to the raw payload. Omitted rather than sent as an\nempty array when there is nothing to state, so presence is the\ntest. An ephemeral `mcp:*` note never carries this, for the same\nreason it carries no subject: its call arguments were hashed at\ncapture and the plaintext never stored.\n\nThis is genealogical content, so it is served only when you pass\n`?include=payload`. A default audit read stays metadata-only."
                },
                "event_type": {
                  "type": "string"
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "kind": {
                  "$ref": "#/components/schemas/AuditEntryKindDto",
                  "description": "Which producer this row came from — see [`AuditEntryKindDto`].\nAlways present: a row whose kind a reader has to infer is a row a\nreader can miscount."
                },
                "payload": {
                  "description": "Extra detail for ephemeral notes only — hashed call arguments and\napproval summaries. Served only when you pass `?include=payload`,\nand never present on a durable entry."
                },
                "subject": {
                  "oneOf": [
                    {
                      "type": "null"
                    },
                    {
                      "$ref": "#/components/schemas/AuditSubjectDto",
                      "description": "The entity this entry changed, when the event names one. Omitted\nrather than null when it does not, so \"is there somewhere to\nnavigate to\" is a presence test.\n\nEphemeral `mcp:*` notes never carry a subject: their call arguments\nwere hashed at capture and the plaintext never stored, so none can\nbe recovered after the fact."
                    }
                  ]
                },
                "via": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Which surface the write arrived over: `\"mcp\"` or `\"http\"`. Set by\nthe service, not by the caller."
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_ConflictDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One detected conflict: whose it is, what is in dispute, and whether it\nis still open.\n\n`claims` carries each disagreeing claim already joined to the source\nthat made it, so a reader can name the dispute without a second\nrequest — the same \"join it in so the reviewer can judge it here\"\nposture `GET /v1/pending`'s `event_context` takes. `assertions` is the\nbare id list, kept for callers that only need identity.",
              "required": [
                "id",
                "scope",
                "assertions",
                "open",
                "claims"
              ],
              "properties": {
                "assertions": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "format": "uuid"
                  }
                },
                "claims": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConflictClaimDto"
                  },
                  "description": "What each side actually claims. Empty only if the conflict names\nassertions the read model no longer holds."
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "open": {
                  "type": "boolean"
                },
                "scope": {
                  "$ref": "#/components/schemas/ConflictScopeDto"
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_JobDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One background job — what `GET /v1/jobs/{id}` returns and what\n`GET /v1/jobs` lists.\n\n`status` is one of `queued`, `running`, `progress`, `done`, `failed`\nor `cancelled`. While `progress`, `percent` and `summary` describe how\nfar it has got; on `done`, `summary` describes the result; on `failed`\nor `cancelled`, `reason` says why. Poll until `status` is `done`,\n`failed` or `cancelled` — those three are terminal.\n\nProgress is reported as counts and named phases only. A job never\nechoes back the content it is processing.\n\n`tree_id` is the tree the work is being done to. A tree only ever\nlists and answers for its own jobs, so this is the tree that was asked\n— never a way to discover what is happening in another one.",
              "required": [
                "id",
                "tree_id",
                "kind",
                "status",
                "created_at_ms",
                "updated_at_ms"
              ],
              "properties": {
                "created_at_ms": {
                  "type": "integer",
                  "format": "int64",
                  "minimum": 0
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "kind": {
                  "type": "string"
                },
                "percent": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "format": "int32",
                  "minimum": 0
                },
                "reason": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "status": {
                  "type": "string",
                  "example": "queued"
                },
                "summary": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "tree_id": {
                  "type": "string"
                },
                "updated_at_ms": {
                  "type": "integer",
                  "format": "int64",
                  "minimum": 0
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_PersonDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "certainty",
                "name",
                "updated_at_ms"
              ],
              "properties": {
                "birth_date": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "description": "The person's recorded birth date, as a structured genealogical\ndate — `{\"kind\": \"exact\"|\"about\"|\"before\"|\"after\"|\"between\",\n\"value\": {…}}`, the same shape a life event's `date` uses. Never a\npreformatted display string: rendering \"abt. 1832\" is the client's\njob, and the uncertainty is part of the data.\n\nDerived from birth events the person's personas take part in as\nthe principal. `null` when there is no such event, or when every\ncandidate is still awaiting review, refuted, or superseded — an\nunreviewed agent claim never becomes a headline identity field.\nWhen several candidates qualify (a merged person whose personas\neach carry a birth), one is chosen deterministically: best\nepistemic status first, then lowest event id."
                },
                "certainty": {
                  "type": "string"
                },
                "death_date": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "description": "The person's recorded death date. Same shape and same eligibility\nrules as `birth_date`, applied to death events. `null` for a living\nperson as well as for an undated one — this field alone does not\ndistinguish \"no death recorded\" from \"known to be living\"; read\n`GET /v1/persons/{id}/living-status` for that."
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string",
                  "description": "The person's display name, derived from the labels of the personas\nconcluded into them — a person record carries no name of its own.\nThis is also what `?q=` searches and what `sort=name` orders by.\nAlways present; an empty string in the rare case no name has been\nderived yet, never omitted."
                },
                "updated_at_ms": {
                  "type": "integer",
                  "format": "int64",
                  "description": "When this person record was last touched, in milliseconds since the\nUnix epoch — what `sort=updated` orders by. A wall-clock reading for\ndisplay, not a total ordering key.",
                  "minimum": 0
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_ProposalDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "status",
                "kind",
                "requested_by",
                "details"
              ],
              "properties": {
                "destination_hint": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Where an `export_everything` proposal says the export should go.\n`null` for every other proposal kind."
                },
                "details": {
                  "$ref": "#/components/schemas/ProposalDetailsDto",
                  "description": "What this proposal would do, in the operation's own fields. Always\npresent, for every kind — an approver is never handed only a verb\nname to judge."
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "kind": {
                  "type": "string"
                },
                "reason": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Why the redaction was proposed. `null` for every other proposal\nkind."
                },
                "requested_by": {
                  "type": "string",
                  "description": "Who filed the proposal: `\"agent\"` or `\"human\"`."
                },
                "status": {
                  "type": "string"
                },
                "target": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid",
                  "description": "The event a `redact` proposal would remove. `null` for every other\nproposal kind."
                },
                "target_summary": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "A short human phrase naming what a `redact` proposal would remove,\nso an approver can tell what they are agreeing to without a second\nlookup. `null` for every other proposal kind, and for a redaction\nwhose target can no longer be resolved — because it was already\nredacted, for instance. Treat a `null` here as \"this record can no\nlonger be described\", not as an error."
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_SourceCitedClaimDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One row of `GET /v1/sources/{id}/cited-by`: a claim that cites this\nsource, carrying enough to name the claim and to navigate to the\nperson it belongs to.\n\n`person` and `person_name` are `null` when the claim's persona has not\nbeen concluded into a person yet — the ordinary state for a freshly\nimported persona. The row still names its persona, so it is never\nblank; nothing is invented to fill the gap.",
              "required": [
                "assertion",
                "claim",
                "kind",
                "status",
                "persona",
                "persona_label"
              ],
              "properties": {
                "assertion": {
                  "type": "string",
                  "format": "uuid"
                },
                "claim": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "description": "What sort of claim this is, as a lowercase string — the same\nvocabulary an assertion's own `kind` uses."
                },
                "person": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid"
                },
                "person_name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "persona": {
                  "type": "string",
                  "format": "uuid"
                },
                "persona_label": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "description": "The claim's epistemic status as a lowercase string — the same\nvocabulary an assertion's own `status` uses."
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_SourceListItemDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One row of `GET /v1/sources`. Deliberately lighter than the\nsingle-source read: enough to list and pick a source, not the full\ndetail. Fetch `GET /v1/sources/{id}` for the citation and the rest.",
              "required": [
                "id",
                "title",
                "claim_count",
                "has_citation"
              ],
              "properties": {
                "claim_count": {
                  "type": "integer",
                  "description": "Claims captured against this source.",
                  "minimum": 0
                },
                "has_citation": {
                  "type": "boolean",
                  "description": "Whether a formal citation is attached."
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "title": {
                  "type": "string"
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "Page_UnsourcedAssertionDto": {
        "type": "object",
        "description": "The pagination envelope every collection route returns:\n`{ \"items\": [...], \"next_cursor\": \"<opaque>\"|null, \"total\": <n> }`.\n\nTo walk a whole collection, pass the `next_cursor` you were given back\nas the `cursor` parameter and repeat until it comes back `null`. Treat\nthe cursor as opaque: do not parse it, and do not reuse one across a\ndifferent sort order or filter — a cursor is only valid against the\nquery that minted it.\n\n`total` is the number of rows the query matched **before** `limit`\nsliced it — the size of the whole filtered set, not of this page. Use\nit for \"showing 25 of 10,725\"; never substitute `items.length`, which\nonly ever tells you the page size.",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One row of `GET /v1/assertions/unsourced`: a claim with no source\nbehind it, together with the persona and person it belongs to, so a\nreview surface can list them without a second request per row.",
              "required": [
                "assertion",
                "persona_label"
              ],
              "properties": {
                "assertion": {
                  "$ref": "#/components/schemas/AssertionDto"
                },
                "person": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid",
                  "description": "The person currently concluded over the assertion's persona, if\nany — `None` when the persona has never been concluded."
                },
                "persona_label": {
                  "type": "string"
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Total rows matching the query across all pages, not just this\none.",
            "minimum": 0
          }
        }
      },
      "ParticipantInputDto": {
        "type": "object",
        "required": [
          "persona",
          "role"
        ],
        "properties": {
          "attached": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "evidence_class": {
            "type": [
              "object",
              "null"
            ]
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "role": {
            "type": "object"
          }
        }
      },
      "ParticipantViewDto": {
        "type": "object",
        "required": [
          "assertion",
          "persona",
          "persona_label",
          "role",
          "status"
        ],
        "properties": {
          "assertion": {
            "type": "string",
            "format": "uuid"
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "persona_label": {
            "type": "string",
            "description": "The persona's display label beside its id, so a reader never needs\na second call to learn who took part."
          },
          "role": {
            "type": "object"
          },
          "status": {
            "type": "string",
            "description": "The epistemic-status discriminant, matching `GET /v1/assertions`'s\n`?state=` vocabulary. What each code means is published once, as\nthe epistemic-status methodology reference, rather than repeated\ninside every participation."
          }
        }
      },
      "ParticipationIdDto": {
        "type": "object",
        "required": [
          "participation_id"
        ],
        "properties": {
          "participation_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "PatchTreeRequest": {
        "type": "object",
        "description": "Body for `PATCH /v1/trees/{tree_id}`. `name` renames the tree;\n`archived` archives or unarchives it. Both fields are optional and\nindependent; a body naming both applies the rename first, then the\narchive flag.",
        "properties": {
          "archived": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PedigreeDto": {
        "type": "object",
        "description": "`GET /v1/persons/{id}/pedigree`'s response: the anchor person plus up\nto `generations` generations of ancestors, as Ahnentafel-numbered\nslots. The anchor is slot 1; the father of slot *k* is slot *2k* and\nthe mother is slot *2k+1*, so a slot number alone tells you the\nrelationship. Unknown ancestors are simply missing from the list —\nthere are no placeholder entries to filter out.",
        "required": [
          "anchor",
          "generations",
          "slots"
        ],
        "properties": {
          "anchor": {
            "type": "string",
            "format": "uuid"
          },
          "generations": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PedigreeSlotDto"
            }
          }
        }
      },
      "PedigreeSlotDto": {
        "type": "object",
        "description": "One filled pedigree slot. `birth_date` and `death_date` use the same\nstructured date shape a person carries. `has_more_ancestors` is true\nexactly where further ancestors exist beyond the requested\n`generations` window, which can only happen on the outermost\ngeneration returned — use it to decide whether asking for more depth\nwould find anything.",
        "required": [
          "slot",
          "person",
          "name",
          "has_more_ancestors"
        ],
        "properties": {
          "birth_date": {
            "type": [
              "object",
              "null"
            ]
          },
          "death_date": {
            "type": [
              "object",
              "null"
            ]
          },
          "has_more_ancestors": {
            "type": "boolean"
          },
          "name": {
            "type": "string"
          },
          "person": {
            "type": "string",
            "format": "uuid"
          },
          "slot": {
            "type": "integer",
            "format": "int64",
            "description": "Ahnentafel number — simultaneously the slot's identity and its\nlayout key.",
            "minimum": 0
          }
        }
      },
      "PendingGroupDto": {
        "type": "object",
        "description": "One bucket of a `group_by=` response. `group_key` is `null` only for\nthe genuine \"no proposer recorded\" bucket when grouping by `proposer`;\ngrouping by `source` always has a key, because every assertion has a\nsource.",
        "required": [
          "count",
          "items"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "group_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssertionDto"
            }
          }
        }
      },
      "PendingLaneBriefDto": {
        "type": "object",
        "description": "How much is waiting in the review queue — counts only. Read\n`GET /v1/pending` for the items themselves.",
        "required": [
          "depth",
          "deferred"
        ],
        "properties": {
          "deferred": {
            "type": "integer",
            "minimum": 0
          },
          "depth": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "PendingPageDto": {
        "type": "object",
        "description": "`GET /v1/pending`'s response. One shape covers both modes:\n\n* Without `group_by`: `items` and `next_cursor` are populated and\n  `groups` is `null` — an ordinary cursor-paginated page.\n* With `group_by`: `groups` is populated, `items` is empty and\n  `next_cursor` is `null`.\n\nGroups are not paginated individually. Each group's `items` is capped\nat `limit` (default 100) and carries no cursor of its own, so a source\nwith more pending items than the cap returns a truncated group.",
        "required": [
          "items"
        ],
        "properties": {
          "groups": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PendingGroupDto"
            }
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssertionDto"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PersonCardAssertionDto": {
        "type": "object",
        "description": "One inlined assertion on an expanded card, carrying the persona label\nand source title beside their ids.",
        "required": [
          "id",
          "persona",
          "persona_label",
          "source",
          "source_title",
          "claim",
          "kind",
          "state"
        ],
        "properties": {
          "claim": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string"
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "persona_label": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          },
          "source_title": {
            "type": "string"
          },
          "state": {
            "type": "string"
          }
        }
      },
      "PersonCardDto": {
        "type": "object",
        "description": "`GET /v1/persons/{id}/card`'s response: one call that orients you on a\nperson — name, lifespan, immediate family, and how much of what is\nclaimed about them is actually sourced. The family links are computed\nserver-side, so there is no need to reconstruct them from raw\nevents.",
        "required": [
          "id",
          "name",
          "parents",
          "spouses",
          "children",
          "assertion_count",
          "source_count"
        ],
        "properties": {
          "assertion_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "assertions": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PersonCardAssertionDto"
            },
            "description": "Present exactly when `expand` asked for `assertions`."
          },
          "birth_year": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PersonSummaryDto"
            }
          },
          "death_year": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "events": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/LifeEventDto"
            },
            "description": "Present exactly when `expand` asked for `events`."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "parents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PersonSummaryDto"
            }
          },
          "source_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "sources": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PersonCardSourceDto"
            },
            "description": "Present exactly when `expand` asked for `sources`."
          },
          "spouses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PersonSummaryDto"
            }
          }
        }
      },
      "PersonCardSourceDto": {
        "type": "object",
        "description": "One inlined source on an expanded card.",
        "required": [
          "id",
          "title",
          "has_citation"
        ],
        "properties": {
          "has_citation": {
            "type": "boolean"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          }
        }
      },
      "PersonDetailDto": {
        "type": "object",
        "description": "`GET /v1/persons/{id}`'s response: everything a list row carries, plus\nthe ids of the personas concluded into this person. Those persona ids\nare what `GET /v1/assertions?persona=` needs, so this read is the\nstarting point for walking a person's claims. The list route returns\nthe lighter row without them.",
        "required": [
          "id",
          "certainty",
          "name",
          "updated_at_ms",
          "personas"
        ],
        "properties": {
          "birth_date": {
            "type": [
              "object",
              "null"
            ],
            "description": "The person's recorded birth date — same structured shape and same\neligibility rules as the list route's `birth_date`."
          },
          "certainty": {
            "type": "string"
          },
          "death_date": {
            "type": [
              "object",
              "null"
            ],
            "description": "The person's recorded death date — same structured shape and same\neligibility rules as the list route's `death_date`."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "The person's display name, derived from the labels of the personas\nconcluded into them."
          },
          "personas": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The identity conclusion's persona set (every persona merged into\nthis person)."
          },
          "updated_at_ms": {
            "type": "integer",
            "format": "int64",
            "description": "When this person record was last touched, in milliseconds since\nthe Unix epoch.",
            "minimum": 0
          }
        }
      },
      "PersonDto": {
        "type": "object",
        "required": [
          "id",
          "certainty",
          "name",
          "updated_at_ms"
        ],
        "properties": {
          "birth_date": {
            "type": [
              "object",
              "null"
            ],
            "description": "The person's recorded birth date, as a structured genealogical\ndate — `{\"kind\": \"exact\"|\"about\"|\"before\"|\"after\"|\"between\",\n\"value\": {…}}`, the same shape a life event's `date` uses. Never a\npreformatted display string: rendering \"abt. 1832\" is the client's\njob, and the uncertainty is part of the data.\n\nDerived from birth events the person's personas take part in as\nthe principal. `null` when there is no such event, or when every\ncandidate is still awaiting review, refuted, or superseded — an\nunreviewed agent claim never becomes a headline identity field.\nWhen several candidates qualify (a merged person whose personas\neach carry a birth), one is chosen deterministically: best\nepistemic status first, then lowest event id."
          },
          "certainty": {
            "type": "string"
          },
          "death_date": {
            "type": [
              "object",
              "null"
            ],
            "description": "The person's recorded death date. Same shape and same eligibility\nrules as `birth_date`, applied to death events. `null` for a living\nperson as well as for an undated one — this field alone does not\ndistinguish \"no death recorded\" from \"known to be living\"; read\n`GET /v1/persons/{id}/living-status` for that."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "The person's display name, derived from the labels of the personas\nconcluded into them — a person record carries no name of its own.\nThis is also what `?q=` searches and what `sort=name` orders by.\nAlways present; an empty string in the rare case no name has been\nderived yet, never omitted."
          },
          "updated_at_ms": {
            "type": "integer",
            "format": "int64",
            "description": "When this person record was last touched, in milliseconds since the\nUnix epoch — what `sort=updated` orders by. A wall-clock reading for\ndisplay, not a total ordering key.",
            "minimum": 0
          }
        }
      },
      "PersonSummaryDto": {
        "type": "object",
        "description": "A one-line reference to a person: id, display name and representative\nyears. Orientation data, not evidence — the dated events behind those\nyears stay reachable through the event reads.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "birth_year": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "death_year": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "PersonaDto": {
        "type": "object",
        "required": [
          "id",
          "source",
          "label"
        ],
        "properties": {
          "concluded_person": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The person this persona has been concluded into, if any. `null`\nwhen the persona has not been matched to anyone yet, which is the\nordinary state for a freshly captured or imported persona — treat\nit as \"not yet matched\", never as an error. Populated only by\n`GET /v1/personas/{id}`."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string"
          },
          "person_birth_date": {
            "type": [
              "object",
              "null"
            ],
            "description": "The concluded person's recorded birth date, in the same shape and\nunder the same rules as a person's own `birth_date`. Present only\nalongside `concluded_person`."
          },
          "person_death_date": {
            "type": [
              "object",
              "null"
            ],
            "description": "The concluded person's recorded death date, in the same shape and\nunder the same rules as a person's own `death_date`. Present only\nalongside `concluded_person`."
          },
          "person_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The concluded person's display name, the same value\n`GET /v1/persons/{id}` reports. Present only alongside\n`concluded_person`."
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "PlanItemAddRequest": {
        "type": "object",
        "description": "`POST /v1/research/questions/{id}/plan/items` request body —\n`research.plan.item.add`'s route.",
        "required": [
          "description"
        ],
        "properties": {
          "description": {
            "type": "string"
          },
          "position": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "source_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "PlanItemAddResponseDto": {
        "type": "object",
        "required": [
          "item_id"
        ],
        "properties": {
          "item_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "PlanItemDescriptionSetRequest": {
        "type": "object",
        "description": "`PUT /v1/research/plan-items/{id}/description`'s request body. The new\ndescription replaces the old wholesale.",
        "required": [
          "description"
        ],
        "properties": {
          "description": {
            "type": "string"
          }
        }
      },
      "PlanItemDoneSetRequest": {
        "type": "object",
        "description": "`PUT /v1/research/plan-items/{id}/done` request body.",
        "required": [
          "done"
        ],
        "properties": {
          "done": {
            "type": "boolean"
          }
        }
      },
      "PlanItemDoneSetResponseDto": {
        "type": "object",
        "required": [
          "item",
          "done"
        ],
        "properties": {
          "done": {
            "type": "boolean"
          },
          "item": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "PlanItemDto": {
        "type": "object",
        "description": "`GET /v1/research/questions/{id}/plan` response — `research.plan.get`'s\nroute.",
        "required": [
          "id",
          "description",
          "position",
          "done",
          "log_entry_count"
        ],
        "properties": {
          "description": {
            "type": "string"
          },
          "done": {
            "type": "boolean"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "log_entry_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "position": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "source_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "PlanItemReorderRequest": {
        "type": "object",
        "description": "`POST /v1/research/plan-items/{id}/reorder` request body.",
        "required": [
          "position"
        ],
        "properties": {
          "position": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "PostTelemetryEventRequest": {
        "type": "object",
        "description": "`POST /v1/telemetry/events`'s request body. `event` is typed as an\nopaque object in this schema, but it is validated strictly on arrival:\nan unrecognized field is rejected, not ignored. Treat the schema's\nlooseness as a documentation gap, not as permission to send anything.",
        "required": [
          "session_id",
          "seq",
          "event"
        ],
        "properties": {
          "event": {},
          "seq": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "session_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "An error body, in RFC 9457 `application/problem+json` form. Every\nnon-2xx response on this API uses this shape.\n\n`type` is the stable, machine-readable identifier for the kind of\nfailure — match on it rather than on `title` or `detail`, which are\nhuman-readable and may be reworded. `extensions` carries any\nadditional members a specific problem type defines (RFC 9457 §3.2),\nflattened onto the top-level object; it is absent entirely for problem\ntypes that define none.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "string"
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "title": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "about:blank"
          }
        }
      },
      "ProofArgumentBriefDto": {
        "type": "object",
        "required": [
          "id",
          "question",
          "conclusion",
          "certainty",
          "recorded_at_ms"
        ],
        "properties": {
          "certainty": {
            "type": "string"
          },
          "conclusion": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string",
            "format": "uuid"
          },
          "recorded_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          }
        }
      },
      "ProofArgumentDto": {
        "type": "object",
        "description": "A proof argument: the written reasoning behind a conclusion, the\nevidence it rests on, and how contrary evidence was handled. Returned\nboth when one is created and when one is read back.",
        "required": [
          "id",
          "question",
          "conclusion",
          "certainty",
          "supporting",
          "correlation_reasoning",
          "contrary_evidence",
          "claims_exhaustive_search"
        ],
        "properties": {
          "certainty": {
            "type": "string"
          },
          "claims_exhaustive_search": {
            "type": "boolean"
          },
          "conclusion": {
            "type": "string"
          },
          "contrary_evidence": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContraryTreatmentView"
            }
          },
          "correlation_reasoning": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string",
            "format": "uuid"
          },
          "supporting": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "ProofArgumentExportDto": {
        "type": "object",
        "required": [
          "schema",
          "id",
          "question",
          "sections",
          "certainty",
          "claims_exhaustive_search",
          "gps_checklist",
          "provenance"
        ],
        "properties": {
          "certainty": {
            "type": "string"
          },
          "claims_exhaustive_search": {
            "type": "boolean"
          },
          "gps_checklist": {
            "$ref": "#/components/schemas/ChecklistViewDto"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "provenance": {
            "$ref": "#/components/schemas/ExportProvenanceDto"
          },
          "question": {
            "$ref": "#/components/schemas/ExportQuestionDto"
          },
          "schema": {
            "type": "string"
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExportSectionDto"
            }
          }
        }
      },
      "ProofArgumentListResponseDto": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProofArgumentDto"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ProofArgumentsSectionDto": {
        "type": "object",
        "required": [
          "total",
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProofArgumentBriefDto"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "ProposalDetailsDto": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "target",
              "reason",
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "redact"
                ]
              },
              "reason": {
                "type": "string"
              },
              "target": {
                "type": "string",
                "format": "uuid"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "destination_hint": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "kind": {
                "type": "string",
                "enum": [
                  "export_everything"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "gedcom_version",
              "include_living",
              "kind"
            ],
            "properties": {
              "destination_hint": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "gedcom_version": {
                "type": "string"
              },
              "include_living": {
                "type": "boolean"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "export_gedcom"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "event",
              "new_date",
              "source",
              "kind"
            ],
            "properties": {
              "event": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "correct_event_date"
                ]
              },
              "new_date": {
                "type": "object"
              },
              "source": {
                "type": "string",
                "format": "uuid"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "event",
              "new_place",
              "source",
              "kind"
            ],
            "properties": {
              "event": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "correct_event_place"
                ]
              },
              "new_place": {
                "type": "object"
              },
              "source": {
                "type": "string",
                "format": "uuid"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "event",
              "new_kind",
              "source",
              "kind"
            ],
            "properties": {
              "event": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "correct_event_kind"
                ]
              },
              "kind_phrase": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "new_kind": {
                "type": "object"
              },
              "source": {
                "type": "string",
                "format": "uuid"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "citation",
              "element_count",
              "kind"
            ],
            "properties": {
              "citation": {
                "type": "string",
                "format": "uuid"
              },
              "element_count": {
                "type": "integer",
                "format": "int32",
                "minimum": 0
              },
              "kind": {
                "type": "string",
                "enum": [
                  "set_citation_elements"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "import_key",
              "kind"
            ],
            "properties": {
              "import_key": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "undo_gedcom_import"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "The conclusion a human is being asked to stand behind: its sentence,\nhow sure the argument claims to be, and the evidence it rests on and\nargues against.",
            "required": [
              "question",
              "conclusion",
              "certainty",
              "correlation_reasoning",
              "supporting",
              "contrary",
              "claims_exhaustive_search",
              "kind"
            ],
            "properties": {
              "certainty": {
                "type": "string"
              },
              "claims_exhaustive_search": {
                "type": "boolean"
              },
              "conclusion": {
                "type": "string"
              },
              "contrary": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "correlation_reasoning": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "record_proof_argument"
                ]
              },
              "question": {
                "type": "string",
                "format": "uuid"
              },
              "supporting": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          },
          {
            "type": "object",
            "description": "The identity a human is being asked to mint: which personas become\none person, and on what argument.",
            "required": [
              "personas",
              "certainty",
              "kind"
            ],
            "properties": {
              "certainty": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "conclude_person"
                ]
              },
              "personas": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "proof_argument": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              }
            }
          }
        ],
        "description": "What a proposal would actually do, in the proposed operation's own\nfields — so an approver can judge the act rather than its name.\n\nTagged by the same `kind` the enclosing proposal reports: read `kind`,\nthen the fields belonging to it. A `record_proof_argument` carries the\nconclusion sentence, its certainty, the correlation reasoning, and the\nassertions it rests on and argues against; a `conclude_person` carries\nthe personas it would fold into one identity. `target`, `reason` and\n`destination_hint` on the proposal itself stay what they have always\nbeen — slots for `redact` and `export_everything` — and are repeated\nhere so every kind is described in one place."
      },
      "ProposalDto": {
        "type": "object",
        "required": [
          "id",
          "status",
          "kind",
          "requested_by",
          "details"
        ],
        "properties": {
          "destination_hint": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where an `export_everything` proposal says the export should go.\n`null` for every other proposal kind."
          },
          "details": {
            "$ref": "#/components/schemas/ProposalDetailsDto",
            "description": "What this proposal would do, in the operation's own fields. Always\npresent, for every kind — an approver is never handed only a verb\nname to judge."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the redaction was proposed. `null` for every other proposal\nkind."
          },
          "requested_by": {
            "type": "string",
            "description": "Who filed the proposal: `\"agent\"` or `\"human\"`."
          },
          "status": {
            "type": "string"
          },
          "target": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The event a `redact` proposal would remove. `null` for every other\nproposal kind."
          },
          "target_summary": {
            "type": [
              "string",
              "null"
            ],
            "description": "A short human phrase naming what a `redact` proposal would remove,\nso an approver can tell what they are agreeing to without a second\nlookup. `null` for every other proposal kind, and for a redaction\nwhose target can no longer be resolved — because it was already\nredacted, for instance. Treat a `null` here as \"this record can no\nlonger be described\", not as an error."
          }
        }
      },
      "ProposeRedactRequest": {
        "type": "object",
        "required": [
          "target",
          "reason"
        ],
        "properties": {
          "reason": {
            "type": "string"
          },
          "target": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "PutBackupConfigRequest": {
        "type": "object",
        "description": "`PUT /v1/backup/config`'s request body: the one location every backup\nof every tree is written to, and the retention policy for it.\nRequires the human token — setting this is the only way a destination\nis ever chosen, and `POST /v1/trees/{tree_id}/backup` takes no\ndestination of its own.",
        "required": [
          "enabled",
          "cadence_hours",
          "retention"
        ],
        "properties": {
          "backup_root_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "cadence_hours": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "enabled": {
            "type": "boolean"
          },
          "retention": {
            "$ref": "#/components/schemas/BackupRetentionDto"
          }
        }
      },
      "PutTelemetryConfigRequest": {
        "type": "object",
        "description": "`PUT /v1/telemetry/config`'s request body: only `export_enabled`/\n`disclosure_acknowledged` are settable by a human — `acknowledged_schema_version`,\n`backlog_purged_at`, and `last_export_at` are daemon-stamped, never\ncaller-supplied. Either field may be omitted to leave it unchanged.",
        "properties": {
          "disclosure_acknowledged": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "export_enabled": {
            "type": [
              "boolean",
              "null"
            ]
          }
        }
      },
      "QuestionCoverageResponseDto": {
        "type": "object",
        "required": [
          "gps"
        ],
        "properties": {
          "gps": {
            "$ref": "#/components/schemas/ChecklistViewDto"
          }
        }
      },
      "QuestionDto": {
        "type": "object",
        "required": [
          "id",
          "question",
          "status",
          "subjects"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "subjects": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SubjectRefDto"
            }
          }
        }
      },
      "QuestionSubjectsSetRequest": {
        "type": "object",
        "description": "`PUT /v1/research/questions/{id}/subjects` request body —\n`research.question.subjects.set`'s route (diff-applied against current\nsubjects, same as the MCP verb).",
        "required": [
          "subjects"
        ],
        "properties": {
          "subjects": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SubjectRefDto"
            }
          }
        }
      },
      "QuestionTextSetRequest": {
        "type": "object",
        "description": "`PUT /v1/research/questions/{id}/text`'s request body. The new text\nreplaces the old wholesale — this is a correction, not an append.",
        "required": [
          "text"
        ],
        "properties": {
          "text": {
            "type": "string"
          }
        }
      },
      "RecordAssociationRequest": {
        "type": "object",
        "description": "`POST /v1/associations` request body.",
        "required": [
          "persona",
          "other",
          "relation",
          "source",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "attached": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "evidence_class": {
            "type": [
              "object",
              "null"
            ]
          },
          "other": {
            "type": "string",
            "format": "uuid"
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "relation": {
            "type": "object"
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RecordLifeEventRequest": {
        "type": "object",
        "description": "`POST /v1/events` request body: `record_life_event`'s input, plus the\nmandatory `ResearchContext` (evidence-creating — same\n`research_session`/`research_search_context` convention as\n`CaptureRequest`).",
        "required": [
          "kind",
          "source",
          "participants",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "date": {
            "type": [
              "object",
              "null"
            ]
          },
          "kind": {
            "type": "object"
          },
          "kind_phrase": {
            "type": [
              "string",
              "null"
            ]
          },
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParticipantInputDto"
            }
          },
          "place": {
            "type": [
              "object",
              "null"
            ]
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RecordProofArgumentRequest": {
        "type": "object",
        "description": "Body for recording a proof argument — the written reasoning that\ncarries a conclusion, together with the evidence for and against it.\n`certainty` and `contrary_evidence` are sent as opaque JSON values\nwhose shapes are not enumerated in this schema; every other field is a\nplain string or id.",
        "required": [
          "question",
          "conclusion",
          "supporting",
          "correlation_reasoning",
          "certainty",
          "contrary_evidence",
          "claims_exhaustive_search"
        ],
        "properties": {
          "certainty": {
            "type": "object"
          },
          "claims_exhaustive_search": {
            "type": "boolean"
          },
          "conclusion": {
            "type": "string"
          },
          "contrary_evidence": {
            "type": "object"
          },
          "correlation_reasoning": {
            "type": "string"
          },
          "question": {
            "type": "string",
            "format": "uuid"
          },
          "supporting": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "RecordProofArgumentResultDto": {
        "type": "object",
        "description": "The outcome of recording a proof argument. A human-token request\nrecords it and returns `200` with the full argument; an agent-token\nrequest files a proposal and returns `202` with a `proposal_id`. Same\nbody shape either way.",
        "required": [
          "applied"
        ],
        "properties": {
          "applied": {
            "type": "boolean"
          },
          "argument": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ProofArgumentDto"
              }
            ]
          },
          "proposal_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "RecordedLifeEventDto": {
        "type": "object",
        "required": [
          "event_id",
          "participation_ids"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "participation_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "RedactionRequest": {
        "type": "object",
        "required": [
          "target",
          "reason",
          "summary_shown"
        ],
        "properties": {
          "reason": {
            "type": "string"
          },
          "summary_shown": {
            "type": "string"
          },
          "target": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RedactionResultDto": {
        "type": "object",
        "required": [
          "redacted",
          "blobs_purged"
        ],
        "properties": {
          "blobs_purged": {
            "type": "integer",
            "minimum": 0
          },
          "redacted": {
            "type": "boolean"
          }
        }
      },
      "RegionDto": {
        "type": "object",
        "description": "A rectangular region of a source image in normalized `[0, 1]`\nfractions of the (orientation-applied) image box — never pixels.\nMust have positive extent and stay inside the image\n(`x + width <= 1`, `y + height <= 1`).",
        "required": [
          "x",
          "y",
          "width",
          "height"
        ],
        "properties": {
          "height": {
            "type": "number",
            "format": "double",
            "description": "Height, as a fraction of image height."
          },
          "width": {
            "type": "number",
            "format": "double",
            "description": "Width, as a fraction of image width."
          },
          "x": {
            "type": "number",
            "format": "double",
            "description": "Left edge, as a fraction of image width."
          },
          "y": {
            "type": "number",
            "format": "double",
            "description": "Top edge, as a fraction of image height."
          }
        }
      },
      "RejectedEvidenceDto": {
        "type": "object",
        "description": "Contrary evidence a recorded proof argument considered and rejected.",
        "required": [
          "proof_argument",
          "assertion",
          "treatment",
          "recorded_by",
          "recorded_at_ms"
        ],
        "properties": {
          "assertion": {
            "type": "string",
            "format": "uuid"
          },
          "proof_argument": {
            "type": "string",
            "format": "uuid"
          },
          "recorded_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "recorded_by": {
            "type": "string"
          },
          "treatment": {
            "type": "string"
          }
        }
      },
      "RelationshipAttachRequest": {
        "type": "object",
        "description": "`POST /v1/relationships`'s request body. `kind` says which\nrelationship to attach between `person` and `relative`: `\"parent\"`\n(`relative` becomes `person`'s parent), `\"child\"` (`relative` becomes\n`person`'s child), or `\"spouse\"` (the two become spouses).",
        "required": [
          "kind",
          "person",
          "relative",
          "source",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "kind": {
            "type": "string"
          },
          "person": {
            "type": "string",
            "format": "uuid"
          },
          "relative": {
            "type": "string",
            "format": "uuid"
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RelationshipOutcomeDto": {
        "type": "object",
        "description": "The outcome of adding or re-pointing a relationship. `created: false`\nmeans the relationship already existed and nothing changed — these\nroutes are idempotent, so a repeat call is a success, not a\nconflict.",
        "required": [
          "event",
          "assertions",
          "created"
        ],
        "properties": {
          "assertions": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "created": {
            "type": "boolean"
          },
          "event": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RelationshipReparentRequest": {
        "type": "object",
        "description": "`POST /v1/relationships/reparent` request body.",
        "required": [
          "child",
          "from_parent",
          "to_parent",
          "source",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "child": {
            "type": "string",
            "format": "uuid"
          },
          "from_parent": {
            "type": "string",
            "format": "uuid"
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": "string",
            "format": "uuid"
          },
          "to_parent": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RelationshipUnlinkRequest": {
        "type": "object",
        "description": "`POST /v1/relationships/unlink` request body — same `kind` vocabulary\nas [`RelationshipAttachRequest`], applied to removal instead of\nattachment.",
        "required": [
          "kind",
          "person",
          "relative",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "kind": {
            "type": "string"
          },
          "person": {
            "type": "string",
            "format": "uuid"
          },
          "relative": {
            "type": "string",
            "format": "uuid"
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RenderedTextDto": {
        "type": "object",
        "required": [
          "text",
          "html"
        ],
        "properties": {
          "html": {
            "type": "string"
          },
          "text": {
            "type": "string"
          }
        }
      },
      "ResearchLogCaptureLinkedDto": {
        "type": "object",
        "properties": {
          "persona": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "plan_item": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "ResearchLogCaptureRequest": {
        "type": "object",
        "description": "`POST /v1/research/log/capture`'s request body: record a search that\nwas performed and what it turned up, in one call — including a search\nthat found nothing, which is evidence too.",
        "required": [
          "search_context"
        ],
        "properties": {
          "found_nothing": {
            "type": "boolean"
          },
          "persona": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "search_context": {
            "type": "string"
          },
          "session": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "ResearchLogCaptureResponseDto": {
        "type": "object",
        "required": [
          "log_entry_id",
          "session_id",
          "linked"
        ],
        "properties": {
          "linked": {
            "$ref": "#/components/schemas/ResearchLogCaptureLinkedDto"
          },
          "log_entry_id": {
            "type": "string",
            "format": "uuid"
          },
          "session_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ResearchLogEntryDto": {
        "type": "object",
        "description": "`GET /v1/research/log/{id}`'s response: one research-log entry in\nfull, with every appended detail — no cap, unlike the list route.",
        "required": [
          "id",
          "session",
          "search_context",
          "attestation",
          "recorded_at",
          "found_nothing",
          "details"
        ],
        "properties": {
          "attestation": {
            "type": "string"
          },
          "details": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LogDetailDto"
            }
          },
          "found_nothing": {
            "type": "boolean"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "persona": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "plan_item": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "recorded_at": {
            "type": "integer",
            "format": "int64"
          },
          "search_context": {
            "type": "string"
          },
          "session": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "ResearchLogListEntryDto": {
        "type": "object",
        "required": [
          "id",
          "session",
          "search_context",
          "attestation",
          "recorded_at",
          "found_nothing",
          "details"
        ],
        "properties": {
          "attestation": {
            "type": "string",
            "description": "Where this entry came from: `user_entered`, `agent_reported` or\n`import_derived`."
          },
          "details": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LogDetailDto"
            },
            "description": "Follow-up details, oldest first, capped at 20 on this list route\nso one heavily-annotated entry cannot swamp a page. Read\n`GET /v1/research/log/{id}` for the complete, uncapped set."
          },
          "found_nothing": {
            "type": "boolean",
            "description": "`true` when this entry records a search that found nothing — a\nnegative result, which is evidence in its own right."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "persona": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "plan_item": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The research question this entry's session was working on, so a\nrow can link back to its question without a second lookup."
          },
          "recorded_at": {
            "type": "integer",
            "format": "int64",
            "description": "When the entry was recorded, in milliseconds since the Unix\nepoch."
          },
          "search_context": {
            "type": "string"
          },
          "session": {
            "type": "string",
            "format": "uuid"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "ResearchLogListResponseDto": {
        "type": "object",
        "required": [
          "entries",
          "total"
        ],
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResearchLogListEntryDto"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer",
            "description": "How many entries match the filter in total, independent of `limit`\nand `cursor`. Use this for \"showing 25 of 10,725\"; the length of\n`entries` only ever tells you the page size.",
            "minimum": 0
          }
        }
      },
      "ResearchPlanGetResponseDto": {
        "type": "object",
        "required": [
          "items",
          "coverage"
        ],
        "properties": {
          "coverage": {
            "$ref": "#/components/schemas/ChecklistViewDto"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanItemDto"
            }
          }
        }
      },
      "ResearchQuestionDto": {
        "type": "object",
        "required": [
          "id",
          "question"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string"
          }
        }
      },
      "ResearchQuestionListResponseDto": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuestionDto"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ResolveConflictRequest": {
        "type": "object",
        "description": "Body for `POST /v1/conflicts/{id}/resolution`.",
        "required": [
          "proof_argument"
        ],
        "properties": {
          "proof_argument": {
            "type": "string",
            "format": "uuid",
            "description": "The proof argument that settles this conflict. A written argument\nis the only thing that resolves a contradiction here — competing\nclaims are never merged or silently picked between — so this must\nname an argument that already exists."
          }
        }
      },
      "ResolveMediaRequest": {
        "type": "object",
        "required": [
          "blob"
        ],
        "properties": {
          "blob": {
            "type": "string",
            "description": "The content address `POST /v1/blobs` returned for these bytes, as\n`sha256:<64 hex chars>`."
          }
        }
      },
      "RestoreRequest": {
        "type": "object",
        "description": "`POST /v1/restore`'s request body: which backup to restore.\n\nThere is deliberately no restore-mode field. A restore always creates\na new tree; accepting a mode this API does not honour would mean\nrestoring differently than asked.",
        "required": [
          "backup_root",
          "tree_id",
          "backup_id"
        ],
        "properties": {
          "backup_id": {
            "type": "string"
          },
          "backup_root": {
            "type": "string"
          },
          "tree_id": {
            "type": "string",
            "description": "The manifest's own `tree_id` — the tree the backup was taken of."
          }
        }
      },
      "RestoredTreeDto": {
        "type": "object",
        "description": "What a restore actually did: the new tree it created, and how much it\nverified on the way.\n\n`objects_verified` and `bytes_verified` are worth surfacing rather\nthan discarding — every one of those bytes was re-hashed against the\nbackup manifest before anything was written, and a verification nobody\nsees is indistinguishable from one that never ran.",
        "required": [
          "tree_id",
          "tree_name",
          "source_tree_id",
          "backup_id",
          "event_count",
          "objects_verified",
          "bytes_verified"
        ],
        "properties": {
          "backup_id": {
            "type": "string"
          },
          "bytes_verified": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "event_count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "objects_verified": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "source_tree_id": {
            "type": "string",
            "description": "The tree id recorded in the backup, which is not the id above."
          },
          "tree_id": {
            "type": "string",
            "description": "The id the restored tree was registered under — a new tree, never\none that already existed."
          },
          "tree_name": {
            "type": "string"
          }
        }
      },
      "RetiredLeadBriefDto": {
        "type": "object",
        "description": "A research question that was closed rather than answered.\n`disposition` and `reason` say how it was retired; both are absent for\nquestions closed before those were recorded.",
        "required": [
          "id",
          "question",
          "closed_at_ms"
        ],
        "properties": {
          "closed_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "disposition": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RetiredLeadDto": {
        "type": "object",
        "description": "A retired lead in full: unlike the brief's capped pointer form, the\nreason is complete, never truncated.",
        "required": [
          "id",
          "question",
          "closed_at_ms"
        ],
        "properties": {
          "closed_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "disposition": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RetiredSectionDto": {
        "type": "object",
        "required": [
          "total",
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RetiredLeadBriefDto"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "RootSuggestionDto": {
        "type": "object",
        "description": "One suggested starting person, best first. `ancestors` and\n`generations` are the ranking evidence, offered so a caller can say\n*why* this person is suggested rather than presenting the pick as an\noracle; both are counted within the pedigree's maximum depth, so\nneither exceeds what a chart could ever show. Suggesting someone\nrecords nothing about them — it is a read, and nothing is stored\nuntil a caller sets the home person.",
        "required": [
          "person",
          "name",
          "ancestors",
          "generations"
        ],
        "properties": {
          "ancestors": {
            "type": "integer",
            "format": "int32",
            "description": "Distinct forebears found above this person.",
            "minimum": 0
          },
          "birth_date": {
            "type": [
              "object",
              "null"
            ]
          },
          "death_date": {
            "type": [
              "object",
              "null"
            ]
          },
          "generations": {
            "type": "integer",
            "format": "int32",
            "description": "How many generations those forebears span.",
            "minimum": 0
          },
          "name": {
            "type": "string"
          },
          "person": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RootSuggestionsDto": {
        "type": "object",
        "description": "`GET /v1/trees/{tree_id}/root-suggestions`'s response: how many people\nthe tree holds, plus a short ranked list of people whose ancestor\nchart would be fullest. Answers the two questions a Family tree\nsurface with no home person set has to answer at once — is there any\ndata here at all, and if so who is worth starting from. A tree with\npeople always yields suggestions; an empty tree yields none, and\n`person_count` of zero is what says which case this is.",
        "required": [
          "person_count",
          "suggestions"
        ],
        "properties": {
          "person_count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "suggestions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RootSuggestionDto"
            }
          }
        }
      },
      "RuledOutAssertionDto": {
        "type": "object",
        "description": "An assertion permanently ruled out of evidence (refuted/superseded).",
        "required": [
          "id",
          "claim",
          "status"
        ],
        "properties": {
          "claim": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "ruled_out_at_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "ruled_out_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "Actor class of the ruling-out transition, absent when the fold\npredates transition provenance."
          },
          "status": {
            "type": "string"
          }
        }
      },
      "SessionBriefDto": {
        "type": "object",
        "description": "The session brief: a bounded digest of the current state of the\ninvestigation, meant to orient a caller starting cold. Pointers and\ncounts rather than full records, so it stays a fixed size no matter\nhow large the tree is; pass `since` to get only what changed.",
        "required": [
          "open_questions",
          "settled",
          "retired",
          "pending",
          "conflicts",
          "rules",
          "cursor"
        ],
        "properties": {
          "conflicts": {
            "$ref": "#/components/schemas/ConflictDetectionBriefDto"
          },
          "cursor": {
            "type": "string"
          },
          "open_questions": {
            "$ref": "#/components/schemas/OpenQuestionsSectionDto"
          },
          "pending": {
            "$ref": "#/components/schemas/PendingLaneBriefDto"
          },
          "retired": {
            "$ref": "#/components/schemas/RetiredSectionDto"
          },
          "rules": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "settled": {
            "$ref": "#/components/schemas/SettledSectionDto"
          },
          "since": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SessionSummaryDto": {
        "type": "object",
        "description": "What one research session covered: its questions, its log entries and\nwhat came of them. Assembled at read time from the log, so it always\nreflects the current state rather than a snapshot taken when the\nsession ended.",
        "required": [
          "session",
          "first_seen_at_ms",
          "last_seen_at_ms",
          "event_count"
        ],
        "properties": {
          "actor_model": {
            "type": [
              "string",
              "null"
            ]
          },
          "actor_reported_harness": {
            "type": [
              "string",
              "null"
            ]
          },
          "current_assignment": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "event_count": {
            "type": "integer",
            "minimum": 0
          },
          "first_seen_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "last_seen_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "session": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "SetActiveTreeRequest": {
        "type": "object",
        "required": [
          "tree_id"
        ],
        "properties": {
          "tree_id": {
            "type": "string"
          }
        }
      },
      "SetCitationElementsRequest": {
        "type": "object",
        "description": "`PUT /v1/citations/{id}/elements` request body.",
        "required": [
          "elements"
        ],
        "properties": {
          "elements": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "propertyNames": {
              "type": "string"
            }
          }
        }
      },
      "SetCitationElementsResultDto": {
        "type": "object",
        "description": "The outcome of setting a citation's elements. A human-token request\napplies it and returns `200`; an agent-token request files a proposal\nand returns `202` with a `proposal_id`. Same body shape either way.",
        "required": [
          "applied"
        ],
        "properties": {
          "applied": {
            "type": "boolean"
          },
          "proposal_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "rendered": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/CitationRenderResultDto"
              }
            ]
          },
          "revision": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "SetCitationSourceTypeRequest": {
        "type": "object",
        "description": "`PUT /v1/citations/{id}/source-type`'s request body: what kind of\nrecord the source actually is. Imports never guess a source type, so\nthis call is how an imported source becomes citable against a\ntemplate.",
        "required": [
          "template_id"
        ],
        "properties": {
          "template_id": {
            "type": "string"
          }
        }
      },
      "SetCitationSourceTypeResultDto": {
        "type": "object",
        "description": "The outcome of setting a citation's source type. A human-token request\napplies it and returns `200`; an agent-token request files a proposal\nand returns `202` with a `proposal_id`. Same body shape either way.",
        "required": [
          "applied"
        ],
        "properties": {
          "applied": {
            "type": "boolean"
          },
          "missing_required": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "proposal_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "revision": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "template_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SetLivingStatusRequest": {
        "type": "object",
        "description": "`PUT /v1/persons/{id}/living-status`'s request body. `status` is one of\n`\"living\"`, `\"deceased\"` or `\"use_heuristic\"`, sent as a JSON value.\nThis overrides the automatic guess that drives privacy suppression in\na GEDCOM export; `\"use_heuristic\"` hands the decision back.",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "object"
          }
        }
      },
      "SetNamePiecesRequest": {
        "type": "object",
        "description": "`PUT /v1/personas/{id}/name-pieces` request body.",
        "required": [
          "pieces",
          "research_session",
          "research_search_context"
        ],
        "properties": {
          "pieces": {
            "type": "object"
          },
          "research_search_context": {
            "type": "string"
          },
          "research_session": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "SettledConclusionDto": {
        "type": "object",
        "description": "One recorded conclusion in the settled surface (`settled.list`).",
        "required": [
          "id",
          "question",
          "question_text",
          "conclusion",
          "certainty",
          "recorded_by",
          "recorded_at_ms"
        ],
        "properties": {
          "certainty": {
            "type": "string"
          },
          "conclusion": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string",
            "format": "uuid"
          },
          "question_text": {
            "type": "string"
          },
          "recorded_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "recorded_by": {
            "type": "string",
            "description": "`human | agent` — actor class only, never a raw variant name."
          }
        }
      },
      "SettledKnowledgeDto": {
        "type": "object",
        "description": "`GET /v1/settled`'s response: everything already decided in this tree,\nin one read — what is concluded, what was refuted or superseded, what\nwas searched for and not found, what was considered and rejected, and\nwhat was retired. Each carries its reason and where it came from.\n\nRead this before proposing work, so effort is not spent re-deriving a\nquestion that has already been closed.",
        "required": [
          "conclusions",
          "refutations",
          "negative_findings",
          "rejected_evidence",
          "retired_leads"
        ],
        "properties": {
          "conclusions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SettledConclusionDto"
            }
          },
          "negative_findings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NegativeFindingDto"
            }
          },
          "refutations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RuledOutAssertionDto"
            }
          },
          "rejected_evidence": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RejectedEvidenceDto"
            }
          },
          "retired_leads": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RetiredLeadDto"
            }
          }
        }
      },
      "SettledSectionDto": {
        "type": "object",
        "required": [
          "confirmed_assertions",
          "proof_arguments"
        ],
        "properties": {
          "confirmed_assertions": {
            "$ref": "#/components/schemas/ConfirmedAssertionsSectionDto"
          },
          "proof_arguments": {
            "$ref": "#/components/schemas/ProofArgumentsSectionDto"
          }
        }
      },
      "SiblingParticipationDto": {
        "type": "object",
        "description": "Another persona taking part in the same life event, joined into a\npending item so a reviewer can see who else is involved. Present only\non `GET /v1/pending`, and only for event-participation claims.",
        "required": [
          "persona_id",
          "persona_label_id",
          "role",
          "status"
        ],
        "properties": {
          "persona_id": {
            "type": "string",
            "format": "uuid"
          },
          "persona_label_id": {
            "type": "string",
            "description": "The other persona's display label. Despite the field name this is\nthe label text itself, not an id."
          },
          "role": {
            "type": "object"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "SourceCitationDto": {
        "type": "object",
        "description": "A source's citation: the typed elements that were recorded, plus the\nthree prose forms rendered from them (first reference, subsequent\nreference, bibliography entry).\n\nThe rendered forms are `null` when the citation has no recognized\nsource type behind it — a free-text or imported citation. That is a\nreal state to display, not an error: the elements are still there to\nrender field by field.",
        "required": [
          "id",
          "elements"
        ],
        "properties": {
          "bibliography": {
            "type": [
              "string",
              "null"
            ]
          },
          "elements": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CitationElementDto"
            },
            "description": "The current element revision, in template order."
          },
          "field_values": {
            "type": [
              "object",
              "null"
            ],
            "description": "The citation's current values keyed by field id, ready to\npre-populate a fill-in form. Restricted to the fields the recorded\nsource type's template actually declares. `null` before any source\ntype is chosen — there is no template to key against, which is\ndifferent from \"keyed, and every value happens to be empty\".\nPrefer this over re-deriving field values from `elements`\nyourself.",
            "additionalProperties": {
              "type": "string"
            },
            "propertyNames": {
              "type": "string"
            }
          },
          "finalized_prose": {
            "type": [
              "string",
              "null"
            ],
            "description": "The human-finalized prose snapshot, if a human has finalized one."
          },
          "first": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "missing_required": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Required fields the recorded source type declares that this\ncitation leaves empty — the reason `first`, `subsequent` and\n`bibliography` can all be `null` even though a type was chosen.\nEmpty when the citation renders fully, and also when no source\ntype has been chosen at all (that case is legible from the missing\n`source_type` element instead). Always present, empty list\nincluded."
          },
          "revision": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Current element-revision number (`1` at creation, incremented by\neach element edit); `None` if the fold has no revision recorded.",
            "minimum": 0
          },
          "subsequent": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SourceCitedClaimDto": {
        "type": "object",
        "description": "One row of `GET /v1/sources/{id}/cited-by`: a claim that cites this\nsource, carrying enough to name the claim and to navigate to the\nperson it belongs to.\n\n`person` and `person_name` are `null` when the claim's persona has not\nbeen concluded into a person yet — the ordinary state for a freshly\nimported persona. The row still names its persona, so it is never\nblank; nothing is invented to fill the gap.",
        "required": [
          "assertion",
          "claim",
          "kind",
          "status",
          "persona",
          "persona_label"
        ],
        "properties": {
          "assertion": {
            "type": "string",
            "format": "uuid"
          },
          "claim": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "What sort of claim this is, as a lowercase string — the same\nvocabulary an assertion's own `kind` uses."
          },
          "person": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "person_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "persona": {
            "type": "string",
            "format": "uuid"
          },
          "persona_label": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "The claim's epistemic status as a lowercase string — the same\nvocabulary an assertion's own `status` uses."
          }
        }
      },
      "SourceDto": {
        "type": "object",
        "required": [
          "id",
          "title",
          "blobs",
          "claim_count"
        ],
        "properties": {
          "blobs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Content addresses only — never a filesystem path."
          },
          "citation": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/SourceCitationDto",
                "description": "The source's citation: its current typed elements plus the prose\nforms rendered from them. `null` for a source with no citation\nattached — an imported or bare-title source — which is a real\nstate to display, not a gap to fill in. Populated only by\n`GET /v1/sources/{id}`."
              }
            ]
          },
          "claim_count": {
            "type": "integer",
            "description": "How many claims cite this source. This is the size of the full\nlist `GET /v1/sources/{id}/cited-by` pages through.",
            "minimum": 0
          },
          "derived_from": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The source this one was derived from, letting a chain be recorded\n(a digital image of a microfilm of an original register). `null`\nwhen no such relationship was recorded."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          }
        }
      },
      "SourceFromTemplateRequest": {
        "type": "object",
        "description": "`POST /v1/sources/from-template` request body.",
        "required": [
          "template_id",
          "elements"
        ],
        "properties": {
          "elements": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "propertyNames": {
              "type": "string"
            }
          },
          "template_id": {
            "type": "string"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SourceFromTemplateResultDto": {
        "type": "object",
        "required": [
          "source_id",
          "citation_id",
          "rendered",
          "created"
        ],
        "properties": {
          "citation_id": {
            "type": "string",
            "format": "uuid"
          },
          "created": {
            "type": "boolean",
            "description": "`true` when a new source was created, `false` when an identical\none already existed and was returned instead. The route is\nfind-or-create, so entering the same record twice never duplicates\nit; the status code (`201` versus `200`) says the same thing."
          },
          "rendered": {
            "$ref": "#/components/schemas/CitationRenderResultDto"
          },
          "source_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "SourceListItemDto": {
        "type": "object",
        "description": "One row of `GET /v1/sources`. Deliberately lighter than the\nsingle-source read: enough to list and pick a source, not the full\ndetail. Fetch `GET /v1/sources/{id}` for the citation and the rest.",
        "required": [
          "id",
          "title",
          "claim_count",
          "has_citation"
        ],
        "properties": {
          "claim_count": {
            "type": "integer",
            "description": "Claims captured against this source.",
            "minimum": 0
          },
          "has_citation": {
            "type": "boolean",
            "description": "Whether a formal citation is attached."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          }
        }
      },
      "SourcingTallyDto": {
        "type": "object",
        "description": "`GET /v1/assertions/unsourced/tally`: the unsourced surface's own\nsplit. `needs_source` is the only field a client may present as\n\"unsourced\"; `total` is `needs_source + sourced` and is serialized so\na client never has to add them itself and get a different answer.",
        "required": [
          "needs_source",
          "sourced",
          "total"
        ],
        "properties": {
          "needs_source": {
            "type": "integer",
            "description": "Claims with no record behind them at all.",
            "minimum": 0
          },
          "sourced": {
            "type": "integer",
            "description": "Claims a record already stands behind, still awaiting verification\n— a citation carried at capture, or evidence attached since.",
            "minimum": 0
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "SplitPersonRequest": {
        "type": "object",
        "description": "`POST /v1/persons/{id}/split`'s request body: which personas go to\neach half when one person turns out to be two. The caller decides the\npartition; nothing is inferred.",
        "required": [
          "left",
          "right"
        ],
        "properties": {
          "left": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "right": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "SplitPersonResultDto": {
        "type": "object",
        "description": "`POST /v1/persons/{id}/split`'s response — the two new person ids.",
        "required": [
          "left",
          "right"
        ],
        "properties": {
          "left": {
            "type": "string",
            "format": "uuid"
          },
          "right": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "StartSessionRequest": {
        "type": "object",
        "required": [
          "question"
        ],
        "properties": {
          "question": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "SubjectRefDto": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "persona"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "person"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "life_event"
                ]
              }
            }
          }
        ],
        "description": "What a research question is about, as `{kind, id}` — a `person`, a\n`persona`, or a `life_event`."
      },
      "SubmitFeedbackRequest": {
        "type": "object",
        "description": "`POST /v1/feedback`'s request body: what the user typed, and nothing\nelse. There is deliberately no field for the app version or platform —\nthe service fills those in itself, so a client cannot misreport which\nbuild a message came from.",
        "required": [
          "message"
        ],
        "properties": {
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional email — used only to reply, never for anything else."
          },
          "message": {
            "type": "string",
            "description": "The user's message. Required, trimmed, ≤ 10 000 characters."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional name, so a reply can be addressed."
          }
        }
      },
      "SupersedeAssertionRequest": {
        "type": "object",
        "description": "`POST /v1/assertions/{id}/supersede`'s request body: the id of the\nassertion that replaces this one. Claims are never edited in place —\nto correct one, capture the new claim and supersede the old, so both\nstay on the record with their order preserved.",
        "required": [
          "by"
        ],
        "properties": {
          "by": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "SurnameCountDto": {
        "type": "object",
        "required": [
          "surname",
          "count"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "surname": {
            "type": "string"
          }
        }
      },
      "TelemetryConfigDto": {
        "type": "object",
        "description": "Whether this installation sends telemetry, and what it has consented\nto. Requires the human token: consent is a decision only a person can\nmake.",
        "required": [
          "export_enabled",
          "disclosure_acknowledged"
        ],
        "properties": {
          "acknowledged_schema_version": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0
          },
          "backlog_purged_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "disclosure_acknowledged": {
            "type": "boolean"
          },
          "export_enabled": {
            "type": "boolean"
          },
          "last_export_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "TelemetryEventQueuedDto": {
        "type": "object",
        "required": [
          "queued"
        ],
        "properties": {
          "queued": {
            "type": "boolean"
          }
        }
      },
      "TelemetryQueuePurgedDto": {
        "type": "object",
        "required": [
          "purged"
        ],
        "properties": {
          "purged": {
            "type": "boolean"
          }
        }
      },
      "TreeDto": {
        "type": "object",
        "description": "One tree, as the tree-management routes report it: its id, its name,\nwhether it is archived, and — for a human-token caller — where it\nlives on disk.",
        "required": [
          "id",
          "name",
          "created_at_ms",
          "archived",
          "store_missing"
        ],
        "properties": {
          "archived": {
            "type": "boolean"
          },
          "created_at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "id": {
            "type": "string"
          },
          "last_modified_at_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The most recent modification time among the tree's stored files,\nin milliseconds since the Unix epoch. `null` under the same\nunreadable-directory condition as `size_bytes`.",
            "minimum": 0
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "description": "The directory holding this tree's data, so a local-first owner can\nfind their own work and open it in a file browser. Computed fresh\non every response, so it is never a stale path.\n\n**Human-token only: `null` for an agent-class caller.** A\nprompt-injectable actor has no use for the user's home directory\nname and disk layout, so it is not served one."
          },
          "name": {
            "type": "string"
          },
          "person_count": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "How many people this tree holds.\n\n`null` when the figure cannot be read — a tree that has never\nbeen opened, or whose file is momentarily locked. Never a guessed\nzero, so treat `null` as \"unknown\", not \"empty\".",
            "minimum": 0
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The tree's on-disk footprint in bytes, summed across every file\nin its store directory. `null` when the directory cannot be read.",
            "minimum": 0
          },
          "store_missing": {
            "type": "boolean",
            "description": "`true` when this tree's data directory is not on this computer —\na disconnected volume, a sync eviction, an interrupted restore.\n\nA reader must branch on this rather than infer loss from\n`person_count`, which is `null` for the ordinary \"not opened yet\"\ncase too. Opening such a tree is refused rather than answered with\na fresh empty store, so this flag and that refusal always agree."
          }
        }
      },
      "TreeStatsDto": {
        "type": "object",
        "description": "`GET /v1/stats`'s response: whole-tree counts, for orienting on a tree\nyou have not seen before.",
        "required": [
          "person_count",
          "source_count",
          "top_surnames",
          "missing_birth_date_count",
          "missing_death_date_count",
          "refused_segments",
          "read_only",
          "read_only_reasons"
        ],
        "properties": {
          "birth_year_max": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "birth_year_min": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "missing_birth_date_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "missing_death_date_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "person_count": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "read_only": {
            "type": "boolean",
            "description": "`true` when every write to this tree is currently refused. The app\nwill not append to a history it cannot fully read.\n\nThree things cause it: a segment this build could not parse, an\nevent type or payload it does not understand, or derived state\nthat fell behind the log. The first two clear by upgrading to a\nbuild that understands the whole history; the third clears when\nthe tree is reopened. The error returned by the refused write says\nwhich one applies."
          },
          "read_only_reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Why, one stable token per cause, so a client can tell the three\napart before a write is attempted: `refused_segments`,\n`unknown_event_types`, `undecodable_events` (all three clear on\nupgrade) and `fold_diverged` (clears on reopen). Empty exactly\nwhen `read_only` is false. A cause with no token here would leave\na client naming a remedy it cannot know is the right one, so an\nunmapped variant is impossible by construction — the mapping is\ntotal over `ReadOnlyReason`."
          },
          "refused_segments": {
            "type": "integer",
            "format": "int32",
            "description": "How many stored segments this build could not parse — an unknown\nor malformed format. Zero on an ordinary tree.",
            "minimum": 0
          },
          "source_count": {
            "type": "integer",
            "format": "int32",
            "description": "How many sources this tree holds. There is deliberately no family\ncount alongside it: families are not a stored entity here, so any\nnumber reported would be an invented one.",
            "minimum": 0
          },
          "top_surnames": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SurnameCountDto"
            }
          }
        }
      },
      "UndoImportRequest": {
        "type": "object",
        "description": "`POST /v1/imports/{import_key}/undo`'s request body. Carries the\nexact summary text the user was shown before they confirmed, so the\nrecord of the undo says what was actually agreed to — not a summary\nreconstructed afterwards.",
        "required": [
          "summary_shown"
        ],
        "properties": {
          "summary_shown": {
            "type": "string"
          }
        }
      },
      "UndoRequest": {
        "type": "object",
        "description": "`POST /v1/undo`'s request body — an explicit `event` targets exactly\nthat edit; omitted resolves to the caller class's most recent\nexecutable edit (the same walk `GET /v1/undo/status` reports).",
        "properties": {
          "event": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "UndoResponse": {
        "type": "object",
        "description": "`POST /v1/undo`'s `200` response.",
        "required": [
          "undone",
          "undone_event_type",
          "compensating"
        ],
        "properties": {
          "compensating": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "undone": {
            "type": "string",
            "format": "uuid"
          },
          "undone_event_type": {
            "type": "string"
          }
        }
      },
      "UndoStatusResponse": {
        "type": "object",
        "description": "`GET /v1/undo/status`'s response: what the next undo would affect and\nwhether it is available. Always scoped to the human's own edit\nhistory.",
        "properties": {
          "last_edit": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/EditDispositionDto"
              }
            ]
          },
          "target": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/UndoTargetDto"
              }
            ]
          }
        }
      },
      "UndoTargetDto": {
        "type": "object",
        "description": "What a `POST /v1/undo` with no target would undo — check this before\noffering an undo affordance, so the label names the right thing.",
        "required": [
          "event",
          "event_type",
          "at_ms"
        ],
        "properties": {
          "at_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "event": {
            "type": "string",
            "format": "uuid"
          },
          "event_type": {
            "type": "string"
          }
        }
      },
      "UndoableNowDto": {
        "type": "object",
        "description": "Whether the last edit can be undone right now. `state` is a closed\nvocabulary — `\"yes\"`, `\"already_undone\"`, `\"deferred\"` or\n`\"not_undoable\"` — and `rationale` is present only for the state that\ncarries an explanation. Branch on `state`, not on which fields happen\nto be set.\n\n`\"deferred\"` carries no tracker id: which issue owns the unwritten\nexecutor is a fact about this repository, not about the reader's tree,\nand a response is not a place to file one.",
        "required": [
          "state"
        ],
        "properties": {
          "rationale": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": "string"
          }
        }
      },
      "UnresolvedMediaDto": {
        "type": "object",
        "description": "One media file this tree references but could not read in on this\nmachine.\n\n`file_name` is the reference's trailing segment only — never the\nfolders above it, which describe the machine the file was exported\nfrom. To supply the bytes, upload them to `POST /v1/blobs` and name the\nreturned address to `POST /v1/media/{id}/resolve`.",
        "required": [
          "media_id",
          "file_name",
          "kind"
        ],
        "properties": {
          "file_name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "What kind of thing the reference points at: `image`, `document`,\n`web_page` or `other`."
          },
          "media_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "UnsourcedAssertionDto": {
        "type": "object",
        "description": "One row of `GET /v1/assertions/unsourced`: a claim with no source\nbehind it, together with the persona and person it belongs to, so a\nreview surface can list them without a second request per row.",
        "required": [
          "assertion",
          "persona_label"
        ],
        "properties": {
          "assertion": {
            "$ref": "#/components/schemas/AssertionDto"
          },
          "person": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The person currently concluded over the assertion's persona, if\nany — `None` when the persona has never been concluded."
          },
          "persona_label": {
            "type": "string"
          }
        }
      },
      "UpdateSettingsDto": {
        "type": "object",
        "description": "Whether the app checks for updates on its own schedule. Turning this\noff stops the background checks only — `POST /v1/update/check` still\nworks when called explicitly.",
        "required": [
          "check_automatically"
        ],
        "properties": {
          "check_automatically": {
            "type": "boolean"
          }
        }
      },
      "UpdateStatusDto": {
        "type": "object",
        "description": "`GET /v1/update/status`'s response: the current and available\nversions plus a state word saying where the update process is. Never a\nfilesystem path — where a staged update lives is not a caller's\nconcern.",
        "required": [
          "state",
          "current_version",
          "channel"
        ],
        "properties": {
          "available_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "channel": {
            "type": "string",
            "description": "`stable` | `early` (CR-D; config-file selected)."
          },
          "current_version": {
            "type": "string"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "error_kind": {
            "type": [
              "string",
              "null"
            ],
            "description": "Stable token naming which leg `error` came from — `check_failed`\n| `download_failed` | `apply_failed`. Display copy is the\nclient's job (the `notice` contract)."
          },
          "last_check_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Unix epoch milliseconds of the last completed check, if any.",
            "minimum": 0
          },
          "notice": {
            "type": [
              "string",
              "null"
            ],
            "description": "A stable token naming a non-error outcome worth telling the user\nabout. The only value today is `staged_already_installed`: a\nstaged update was discarded because that version is already the one\nrunning. Match on the token and choose your own wording, as with\n`state`."
          },
          "staged_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "The verified, staged, pending version (set exactly when\n`state == \"ready\"`)."
          },
          "state": {
            "type": "string",
            "description": "`idle` | `checking` | `downloading` | `ready` | `error`."
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "persons",
      "description": "People in the tree, including the fan-network relationship view."
    },
    {
      "name": "personas",
      "description": "A person's per-source name/identity claims."
    },
    {
      "name": "sources",
      "description": "Source records citations are built from."
    },
    {
      "name": "citations",
      "description": "Citation templates, rendering, and structured elements."
    },
    {
      "name": "assertions",
      "description": "Evidence-backed claims: capture, confirm, refute, attach evidence."
    },
    {
      "name": "research",
      "description": "Research questions, sessions, plans, and the research log (GPS reasonably-exhaustive-search coverage)."
    },
    {
      "name": "pending",
      "description": "The pending lane: assertions awaiting confirmation, deferral, or bulk action."
    },
    {
      "name": "proposals",
      "description": "Merge proposals: propose, approve, deny."
    },
    {
      "name": "redactions",
      "description": "Redacting a record."
    },
    {
      "name": "jobs",
      "description": "Background jobs (currently GEDCOM import)."
    },
    {
      "name": "events",
      "description": "Life events: creation, participants, corrections, and the live change-event stream."
    },
    {
      "name": "audit",
      "description": "The audit trail."
    },
    {
      "name": "conflicts",
      "description": "Detected conflicts between assertions."
    },
    {
      "name": "blobs",
      "description": "Content-addressed binary evidence storage."
    },
    {
      "name": "proof-arguments",
      "description": "Proof arguments: assembling and exporting a case for a conclusion."
    },
    {
      "name": "gedcom",
      "description": "GEDCOM (family file) import, export, validation, and loss reports."
    },
    {
      "name": "associations",
      "description": "Associations between persons."
    },
    {
      "name": "notes",
      "description": "Free-text notes."
    },
    {
      "name": "backup",
      "description": "Backups of a tree, and restoring one into a new tree."
    },
    {
      "name": "capabilities",
      "description": "The daemon's advertised capabilities."
    },
    {
      "name": "diagnostics",
      "description": "Diagnostics summary and downloadable bundle."
    },
    {
      "name": "integrations",
      "description": "Third-party integration setup (Claude Desktop)."
    },
    {
      "name": "updates",
      "description": "The auto-updater: status, check, restart."
    },
    {
      "name": "telemetry",
      "description": "The local, never-leaves-the-machine telemetry queue."
    },
    {
      "name": "trees",
      "description": "Tree management: create, rename, archive, and the active-tree selection."
    },
    {
      "name": "backup",
      "description": "Tree backup: on-demand snapshot, configuration, and status."
    }
  ]
}
