{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/space-ocr-com/json-schema/space-ocr-com-field-spec-schema.json",
  "title": "FieldSpec",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/space-ocr-com-openapi.json#/components/schemas/FieldSpec",
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "type": {
      "type": "string",
      "description": "Containers are `array` / `object`; the default is `string`. Declaring a type never reformats `values` and the type is never shown to the model — `values` stays the model's reading of the page, held to the printed text by a normalized character cross-check (width / brackets / whitespace folded), not a byte-for-byte copy; for exact string reconciliation use `cells[path].evidence.printed_text`. A scalar type (`number` / `integer` / `date`) additionally produces `normalized`: the same reading parsed into that type, shaped exactly like `values`. A value that will not parse is null there, the reason lands at `cells[path].normalized.error`, and the cell is flagged `type_mismatch`. Any other string is accepted, read as string, and reported back in `review.notes`."
    },
    "description": {
      "type": "string"
    },
    "children": {
      "type": "array",
      "items": {}
    },
    "required": {
      "type": "boolean",
      "description": "Review declaration: when this field comes back empty or missing, review.flagged reports it with reason \"missing\". Never shown to the model — extraction is unchanged."
    },
    "label": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      ],
      "description": "The label printed next to the value (e.g. \"合計\"), or an array of candidates. When the value repeats on the page, coordinates anchor to the occurrence beside this label. Labels spanning several words (\"消費税（8%）\") work as written. Top-level non-container fields only (string / number / integer / date) — a label on an array or object, or on any of its children, is ignored, since a label printed once cannot say which repeating row a value belongs to; line-item children are already anchored within their own row. Fires only when the label is printed exactly once; otherwise the normal search is used and review.notes reports it as issue \"label_unresolved\" (the value still comes back, so that note is the only way to see the declaration is inert). Never shown to the model — text is unchanged, only the coordinate anchor moves."
    },
    "pattern": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      ],
      "description": "Regex the NORMALIZED value must satisfy (JSON-Schema semantics: unanchored, use ^…$ for a whole-value rule). String fields only. An array means any-of. Matching runs on the width-folded value, so a plain-ASCII pattern works against a full-width page. Violation → review reason \"pattern_mismatch\"."
    },
    "min": {
      "type": "number",
      "description": "Inclusive lower bound for a number/integer field, checked against the normalized value. Violation → review reason \"out_of_range\"."
    },
    "max": {
      "type": "number",
      "description": "Inclusive upper bound for a number/integer field, checked against the normalized value. Violation → review reason \"out_of_range\"."
    },
    "enum": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "The slot for handing the API a value set your business already owns — a vendor master's company names, an item catalogue, a unit list (袋/本/個). A normalized value outside the set raises review reason \"pattern_mismatch\". Also the only handle on misreads both engines agree on (冊→申), which character cross-checking cannot see. It cannot tell two legitimate set members apart (a registered-but-wrong vendor still passes) — that spot is `near`'s job."
    },
    "review": {
      "type": "string",
      "enum": [
        "normal",
        "off"
      ],
      "description": "Review sensitivity. \"off\" withholds the reasons the ENGINE infers (text_mismatch, low_ratio, ambiguous_occurrence, …) for this field — the cell keeps all its evidence, only the verdict is withheld. It does NOT silence \"missing\" or a constraint violation: those are rules the caller declared. Use on free-text columns (品名/備考) so the signal on 登録番号/合計 stays readable."
    },
    "near": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        {
          "type": "object",
          "properties": {
            "terms": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            },
            "match": {
              "type": "string"
            }
          },
          "required": [
            "terms"
          ]
        }
      ],
      "description": "Printed vocabulary this value is expected to sit beside — one term, a list of alternatives, or { terms, match } (御中/様 for an addressee, 登録番号/〒/TEL for an issuer block). The check runs over EVERY occurrence of the value: none beside a declared term raises review reason \"near_mismatch\" (wrong wherever it is printed), one beside a term but not the copy the coordinates landed on raises \"near_ambiguous\" (which copy is meant is undecided). The arithmetic rides in cells[path].evidence.near. When no declared term is printed anywhere the check abstains and review.notes reports \"near_unresolved\". Never shown to the model — it cannot make the pick right, it makes a wrong pick visible."
    },
    "not_near": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        {
          "type": "object",
          "properties": {
            "terms": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            },
            "match": {
              "type": "string"
            }
          },
          "required": [
            "terms"
          ]
        }
      ],
      "description": "The mirror of `near` — printed vocabulary this value must NOT sit beside (declare 登録番号/TEL/〒 on an addressee name). Sitting beside one raises review reason \"near_conflict\", with the matched term and distance in cells[path].evidence.not_near. Reaches the forms `near` cannot: a form with no addressee line prints no 御中, so `near` can only abstain, while an issuer block always prints something. Absence of the vocabulary is not a violation, so this never abstains and files no note. Never shown to the model."
    }
  },
  "required": [
    "name"
  ]
}