<!-- GENERATED from compat-report.v1.schema.json (pin 18e09a2914241e790fd45faa6385ec591023ea15d37762fa8c3c1ff499b5dd56) — do not edit -->
<!-- Regenerate with `npm run gen:schema-docs`. CI regenerates and diffs, so a hand edit fails the build. -->

This page is generated from the machine schema, property by property, so it cannot drift from it. For the narrative version — how the verdict is derived, what the warning-code ranges mean, `jq` recipes for gating CI — read the [compatibility report guide](/docs/compat-report).

## Identity

| Field | Value |
|---|---|
| Dialect | `https://json-schema.org/draft/2020-12/schema` |
| `$id` | `https://rendlio.com/schemas/compat-report/v1.json` |
| Served at | [`/schemas/compat-report/v1.json`](/schemas/compat-report/v1.json) — byte-identical to the vendored file |
| Title | Rendlio Compatibility Report |
| Root type | `object`, `additionalProperties: false` |
| Top-level properties | 13, of which 13 required |
| Vendored pin | engine commit `8f2013b`, sha256 `18e09a291424`… |

## Family scope

> Family scope: the envelope core (schemaVersion, engine, input, options, mode, result, warnings, timings) is shared by every Rendlio engine; the remaining sections are grid-engine payload. This schema is never extended to carry another format.

That is a normative `$comment` in the schema itself, not a note added here: `compat-report.v1` never mutates. A future engine that needs a different envelope gets a new `$id` and a sibling schema, so a validator pinned to v1 keeps working.

## Top-level properties

| Property | Type | Required | Description |
|---|---|---|---|
| `schemaVersion` | `string` | yes | Version of this report schema, semver-like `1.x`. Additive optional fields bump the minor; anything else bumps the major and gets a new schema URL. *(pattern `^1\.[0-9]+$`)* |
| `engine` | `object` | yes | Identity of the build that produced the report. |
| `input` | `object` | yes | Identity of the bytes that were read, so a report can be tied back to an exact input. |
| `options` | `object` | yes | Canonicalized **effective** options — an exact echo of the CLI flag values including defaults, plus `licenseMode` (`"licensed:<tier>"` or `"evaluation"`). No other licence data ever appears in a report. Deliberately unconstrained so a new flag does not need a schema change. |
| `mode` | `"convert"` \| `"diagnose"` | yes | Which mode produced the report. `diagnose` never writes documents and additionally records `rendered` entries. |
| `result` | `object` | yes | The run’s verdict and the process exit code, always in agreement. |
| `pages` | `object` | yes | Output pagination. |
| `sheets` | `array` of `object` | yes | One entry per sheet in the workbook, in workbook order. |
| `features` | `array` of `object` | yes | The heart of the report: one entry per (feature, status) pair, enforcing the engine-wide rule that nothing is ever silently approximated. In `convert` mode only deviations are listed, so an empty array means nothing deviated; `diagnose` additionally lists `rendered` entries. |
| `fonts` | `object` | yes | Font resolution outcome. Substitutions are called out because they shift column widths and line breaks. |
| `formulas` | `object` | yes | Formula-cell accounting. |
| `warnings` | `array` of `object` | yes | Coded warnings. Codes are owned by engine modules in fixed ranges, listed in the [guide](/docs/compat-report#warnings-and-w-code-ranges). |
| `timings` | `object` | yes | Wall-clock milliseconds per phase. Reported for observability; not a determinism guarantee. |

## `schemaVersion`

Version of this report schema, semver-like `1.x`. Additive optional fields bump the minor; anything else bumps the major and gets a new schema URL.

Type `string` · **required** · pattern `^1\.[0-9]+$`.

## `engine`

Identity of the build that produced the report.

Type `object` · **required** · `additionalProperties: false`.

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `"Rendlio"` (const) | yes | Producing engine. Always the same value — a report is never emitted by anything else. |
| `version` | `string` | yes | Engine semver, as printed by `rendlio version`. |
| `releaseDate` | `string` | yes | ISO date of the build. Licence maintenance windows are compared against this, not against the clock. *(format `date`)* |

## `input`

Identity of the bytes that were read, so a report can be tied back to an exact input.

Type `object` · **required** · `additionalProperties: false`.

| Field | Type | Required | Description |
|---|---|---|---|
| `sha256` | `string` | yes | SHA-256 of the input bytes, lower-case hex. *(pattern `^[0-9a-f]{64}$`)* |
| `sizeBytes` | `integer` | yes | Length of the input in bytes. *(minimum 0)* |
| `fileName` | `string` | no | Original file name. Omitted when the input arrived on stdin. |

## `options`

Canonicalized **effective** options — an exact echo of the CLI flag values including defaults, plus `licenseMode` (`"licensed:<tier>"` or `"evaluation"`). No other licence data ever appears in a report. Deliberately unconstrained so a new flag does not need a schema change.

Type `object` · **required** · free-form — no constrained properties.

## `mode`

Which mode produced the report. `diagnose` never writes documents and additionally records `rendered` entries.

Type `"convert"` \| `"diagnose"` · **required**.

**`mode` values** — all 2, exhaustive:

| Value | Meaning |
|---|---|
| `"convert"` | A conversion run. Writes documents; the report lists deviations only. |
| `"diagnose"` | Analysis only. Writes no documents and additionally records `rendered` entries. |

## `result`

The run’s verdict and the process exit code, always in agreement.

Type `object` · **required** · `additionalProperties: false`.

| Field | Type | Required | Description |
|---|---|---|---|
| `status` | `"success"` \| `"warnings"` \| `"unsupported-content"` \| `"invalid-input"` \| `"resource-limit"` \| `"error"` | yes | Verdict, derived by the ladder documented in the [compatibility report guide](/docs/compat-report#result-status--exit-code-derivation). |
| `exitCode` | `integer` | yes | Process exit code, matching `status`: 0 success, 1 warnings, 2 unsupported content, 3 invalid input, 4 resource limit, 5 internal error. *(minimum 0, maximum 5)* |

**`result.status` values** — all 6, exhaustive:

| Value | Meaning |
|---|---|
| `"success"` | Faithful throughout — no warnings, nothing approximated. |
| `"warnings"` | Converted, with at least one warning or one `approximated` feature. |
| `"unsupported-content"` | Converted, but at least one feature was `unsupported`. |
| `"invalid-input"` | Not a workbook this engine can read — wrong format, or encrypted. |
| `"resource-limit"` | A limit fired (timeout, memory, size). Partial outputs are deleted. |
| `"error"` | Internal fault. Report the input and this report. |

## `pages`

Output pagination.

Type `object` · **required** · `additionalProperties: false`.

| Field | Type | Required | Description |
|---|---|---|---|
| `count` | `integer` | yes | Rendered page count. `0` for `diagnose` and for failures. *(minimum 0)* |

## `sheets`

One entry per sheet in the workbook, in workbook order.

Type `array` of `object` · **required** · `additionalProperties: false`.

| Item field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Sheet name as stored in the workbook. |
| `index` | `integer` | yes | Zero-based position of the sheet in the workbook. *(minimum 0)* |
| `printed` | `boolean` | yes | Whether the sheet contributed pages. `false` = hidden, or entirely outside the print area. |

## `features`

The heart of the report: one entry per (feature, status) pair, enforcing the engine-wide rule that nothing is ever silently approximated. In `convert` mode only deviations are listed, so an empty array means nothing deviated; `diagnose` additionally lists `rendered` entries.

Type `array` of `object` · **required** · `additionalProperties: false`.

| Item field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Stable feature identifier `F-<AREA>-<slug>`, where `<AREA>` is one of `PARSE`, `STY`, `NUM`, `LAY`, `CF`, `CHT`, `IMG`, `TXT`, `PDF`, `PNG`, `REC`, `LIC`, `SEC`. Identifiers are never repurposed. *(pattern `^F-(PARSE\|STY\|NUM\|LAY\|CF\|CHT\|IMG\|TXT\|PDF\|PNG\|REC\|LIC\|SEC)-[a-z0-9-]+$`)* |
| `status` | `"rendered"` \| `"approximated"` \| `"unsupported"` \| `"skipped"` | yes | What happened to this feature — see the value table below. |
| `count` | `integer` | yes | Occurrences of this (feature, status) pair. Always the real total, even when `locations` is truncated. *(minimum 1)* |
| `locations` | `array` of [`location`](#location) | no | Where the occurrences are. Capped; `count` keeps the real total. *(at most 20 items)* |
| `locationsTruncated` | `boolean` | no | `true` when more occurrences existed than `locations` could list. |
| `detail` | `string` | no | Plain-language statement of the rule that applied. Static template text plus counts and locations only — never cell contents, so a `diagnose` report is safe to share. |

**`features[].status` values** — all 4, exhaustive:

| Value | Meaning |
|---|---|
| `"rendered"` | Faithful per the specification rule for this feature. Recorded in `diagnose` mode only. |
| `"approximated"` | Rendered, but a documented deviation applies — `detail` says which. |
| `"unsupported"` | Present in the file, not rendered; the space it would occupy follows the rule for that feature. |
| `"skipped"` | Present but intentionally outside conversion semantics — not a fidelity gap. |

## `fonts`

Font resolution outcome. Substitutions are called out because they shift column widths and line breaks.

Type `object` · **required** · `additionalProperties: false`.

| Field | Type | Required | Description |
|---|---|---|---|
| `substitutions` | `array` of `object` | yes | Every family that was requested but not used, with what replaced it. |
| `missing` | `array` of `string` | yes | Requested families with no substitute rule at all. |

### `fonts.substitutions[]`

Type `array` of `object` · `additionalProperties: false`.

| Item field | Type | Required | Description |
|---|---|---|---|
| `requested` | `string` | yes | Family name asked for by the workbook. |
| `used` | `string` | yes | Family actually rendered with. |
| `reason` | `"metric-substitute"` \| `"fallback"` \| `"missing-default"` | yes | Why the substitution happened — see the value table below. |

**`fonts.substitutions[].reason` values** — all 3, exhaustive:

| Value | Meaning |
|---|---|
| `"metric-substitute"` | Replaced by a metrically compatible family; layout is preserved. |
| `"fallback"` | Replaced by a generic fallback; glyph shapes and metrics differ. |
| `"missing-default"` | The workbook’s default family was unavailable — expect column widths and line breaks to shift. |

## `formulas`

Formula-cell accounting.

Type `object` · **required** · `additionalProperties: false`.

| Field | Type | Required | Description |
|---|---|---|---|
| `cellsTotal` | `integer` | yes | Number of formula cells in the workbook. *(minimum 0)* |
| `cachedMissing` | `integer` | yes | Formula cells carrying no cached value. In `off` recalc mode these render empty. *(minimum 0)* |
| `recalcMode` | `"off"` \| `"strict-v1"` | yes | Recalculation mode in force — see the value table below. |
| `closureSize` | `integer` \| `null` | no | Size of the evaluation closure when `strict-v1` evaluated or refused; `null` otherwise. *(minimum 0)* |

**`formulas.recalcMode` values** — all 2, exhaustive:

| Value | Meaning |
|---|---|
| `"off"` | Cached formula values are rendered as stored. Cells with no cached value render empty. |
| `"strict-v1"` | The engine evaluated the formula closure itself, or refused and said so. |

## `warnings`

Coded warnings. Codes are owned by engine modules in fixed ranges, listed in the [guide](/docs/compat-report#warnings-and-w-code-ranges).

Type `array` of `object` · **required** · `additionalProperties: false`.

| Item field | Type | Required | Description |
|---|---|---|---|
| `code` | `string` | yes | Warning code, `W` followed by four digits. *(pattern `^W[0-9]{4}$`)* |
| `severity` | `"info"` \| `"warn"` \| `"error"` | yes | How much the warning matters — see the value table below. |
| `message` | `string` | yes | Static template text plus counts and locations only — never cell contents. |
| `location` | [`location`](#location) object | no | Where the warning applies. Omitted, or all-null, for a document-scope warning. |

**`warnings[].severity` values** — all 3, exhaustive:

| Value | Meaning |
|---|---|
| `"info"` | Worth knowing; nothing to act on. |
| `"warn"` | Output may differ from the workbook — check the named location. |
| `"error"` | Something did not work; read `result.status` for the verdict. |

## `timings`

Wall-clock milliseconds per phase. Reported for observability; not a determinism guarantee.

Type `object` · **required** · `additionalProperties: false`.

| Field | Type | Required | Description |
|---|---|---|---|
| `parseMs` | `number` | yes | Time spent reading and parsing the workbook. *(minimum 0)* |
| `layoutMs` | `number` | yes | Time spent paginating and laying out. *(minimum 0)* |
| `renderMs` | `number` | yes | Time spent producing output bytes. *(minimum 0)* |
| `totalMs` | `number` | yes | Total wall-clock time for the run. *(minimum 0)* |

## `location`

A place in the workbook. At least one property must be present; each may be `null` when it does not apply.

Type `object` · at least 1 property present · `additionalProperties: false`.

Referenced by `features[].locations[]`, `warnings[].location`.

| Field | Type | Required | Description |
|---|---|---|---|
| `sheet` | `string` \| `null` | no | Sheet name, or `null` for a workbook-scope location. |
| `ref` | `string` \| `null` | no | A1 cell or range — `"B7"`, `"A1:C30"` — or `null`. |
| `objectId` | `string` \| `null` | no | Drawing/chart part-relative id, or `null`. |

## Every enum in the schema

All 7 closed value sets in v1, read straight out of the schema. Nothing outside these lists is valid.

| Path | Kind | Values |
|---|---|---|
| `engine.name` | const | `"Rendlio"` |
| `mode` | enum | `"convert"` \| `"diagnose"` |
| `result.status` | enum | `"success"` \| `"warnings"` \| `"unsupported-content"` \| `"invalid-input"` \| `"resource-limit"` \| `"error"` |
| `features[].status` | enum | `"rendered"` \| `"approximated"` \| `"unsupported"` \| `"skipped"` |
| `fonts.substitutions[].reason` | enum | `"metric-substitute"` \| `"fallback"` \| `"missing-default"` |
| `formulas.recalcMode` | enum | `"off"` \| `"strict-v1"` |
| `warnings[].severity` | enum | `"info"` \| `"warn"` \| `"error"` |

## Annotated example

A complete report — every top-level property, and every optional field the schema allows. The generator validates it against the schema with `ajv` before writing this page, so it is a report the schema accepts. Real reports are plain JSON; the `//` comments are annotation only.

```jsonc
{
  "schemaVersion": "1.0",
  "engine": {
    "name": "Rendlio",                          // always "Rendlio"
    "version": "0.3.1",
    "releaseDate": "2026-10-02"
  },
  "input": {
    "sha256": "fd646a748c6397ce85167b4763e8f5ee037caac1c33318c48d6955ae01837993",  // of the input bytes, not of the output
    "sizeBytes": 183422,
    "fileName": "quarterly-close.xlsx"          // optional
  },
  "options": {                                  // abridged; the real echo carries every effective flag
    "report": "-",
    "recalc": "off",
    "sheets": "all",
    "licenseMode": "evaluation"                 // the only licence data a report ever carries
  },
  "mode": "diagnose",                           // "convert" | "diagnose"
  "result": {
    "status": "unsupported-content",            // one of 6 — see the `result.status` values table
    "exitCode": 2                               // always agrees with status
  },
  "pages": {
    "count": 0                                  // 0 for diagnose and for failures
  },
  "sheets": [
    {
      "name": "Summary",
      "index": 0,
      "printed": true                           // false = hidden or outside the print area
    },
    {
      "name": "Data",
      "index": 1,
      "printed": true
    },
    {
      "name": "Archive",
      "index": 2,
      "printed": false
    }
  ],
  "features": [                                 // convert mode lists deviations only; [] = nothing deviated
    {
      "id": "F-NUM-number-format",
      "status": "rendered",                     // one of 4 — see the `features[].status` values table
      "count": 41,                              // real total, even when locations is truncated
      "detail": "Excel display strings reproduced with the locale pinned to the workbook setting."  // optional
    },
    {
      "id": "F-STY-cell-fill",
      "status": "rendered",
      "count": 218,
      "detail": "Solid, pattern, and gradient fills painted from the workbook theme."
    },
    {
      "id": "F-LAY-merged-cell",
      "status": "rendered",
      "count": 12,
      "detail": "Merged ranges drawn as a single cell with the anchor cell's alignment."
    },
    {
      "id": "F-CF-time-period",
      "status": "approximated",
      "count": 3,
      "locations": [                            // optional
        {
          "sheet": "Summary",
          "ref": "B4:B31"
        },
        {
          "sheet": "Data",
          "ref": "H2:H480"
        }
      ],
      "detail": "timePeriod rules are evaluated against the conversion date, not the workbook's last-open date."
    },
    {
      "id": "F-CHT-3d-surface",
      "status": "unsupported",
      "count": 2,
      "locations": [
        {
          "sheet": "Summary",
          "objectId": "drawing1:chart2"
        },
        {
          "sheet": "Data",
          "objectId": "drawing2:chart1"
        }
      ],
      "detail": "3-D surface charts are outside the supported chart set; the plot area is rendered empty."
    },
    {
      "id": "F-IMG-vml-extrusion",
      "status": "unsupported",
      "count": 1,
      "locations": [
        {
          "sheet": "Archive",
          "objectId": "vmlDrawing1:shape7"
        }
      ],
      "detail": "Legacy VML 3-D extrusion is outside the supported drawing set; the space it occupies is rendered empty."
    },
    {
      "id": "F-SEC-vba-project",
      "status": "skipped",
      "count": 1,
      "detail": "VBA projects are never executed and never rendered; this is not a fidelity gap."
    },
    {
      "id": "F-REC-data-validation",
      "status": "skipped",
      "count": 27,
      "locations": [
        {
          "sheet": "Data",
          "ref": "C2"
        },
        {
          "sheet": "Data",
          "ref": "C3"
        },
        {
          "sheet": "Data",
          "ref": "C4"
        },
        {
          "sheet": "Data",
          "ref": "C5"
        },
        {
          "sheet": "Data",
          "ref": "C6"
        },
        {
          "sheet": "Data",
          "ref": "C7"
        },
        {
          "sheet": "Data",
          "ref": "C8"
        },
        {
          "sheet": "Data",
          "ref": "C9"
        },
        {
          "sheet": "Data",
          "ref": "C10"
        },
        {
          "sheet": "Data",
          "ref": "C11"
        },
        {
          "sheet": "Data",
          "ref": "C12"
        },
        {
          "sheet": "Data",
          "ref": "C13"
        },
        {
          "sheet": "Data",
          "ref": "C14"
        },
        {
          "sheet": "Data",
          "ref": "C15"
        },
        {
          "sheet": "Data",
          "ref": "C16"
        },
        {
          "sheet": "Data",
          "ref": "C17"
        },
        {
          "sheet": "Data",
          "ref": "C18"
        },
        {
          "sheet": "Data",
          "ref": "C19"
        },
        {
          "sheet": "Data",
          "ref": "C20"
        },
        {
          "sheet": "Data",
          "ref": "C21"
        }
      ],
      "locationsTruncated": true,               // optional
      "detail": "Data-validation rules have no static representation and are intentionally outside conversion semantics."
    }
  ],
  "fonts": {
    "substitutions": [
      {
        "requested": "Calibri",
        "used": "Carlito",
        "reason": "metric-substitute"           // "metric-substitute" | "fallback" | "missing-default"
      },
      {
        "requested": "Wingdings",
        "used": "DejaVu Sans",
        "reason": "fallback"
      },
      {
        "requested": "Aptos Narrow",
        "used": "DejaVu Sans",
        "reason": "missing-default"
      }
    ],
    "missing": [
      "Marlett"
    ]
  },
  "formulas": {
    "cellsTotal": 4183,
    "cachedMissing": 0,                         // render empty while recalcMode is "off"
    "recalcMode": "off",                        // "off" | "strict-v1"
    "closureSize": null                         // integer once strict-v1 evaluates or refuses · optional
  },
  "warnings": [
    {
      "code": "W1011",
      "severity": "warn",                       // "info" | "warn" | "error"
      "message": "External workbook link rendered from its cached value; no network refresh was attempted.",
      "location": {                             // all-null = document scope · optional
        "sheet": "Summary",
        "ref": "E12"
      }
    },
    {
      "code": "W2104",
      "severity": "info",
      "message": "Custom number format approximated to the nearest supported pattern."
    },
    {
      "code": "W5104",
      "severity": "warn",
      "message": "Requested font is not installed; a metric-compatible substitute was used."
    },
    {
      "code": "W7002",
      "severity": "error",
      "message": "A page exceeded the PDF media box and was clipped to the print area.",
      "location": {
        "sheet": "Data"
      }
    },
    {
      "code": "W9001",
      "severity": "info",
      "message": "No licence found; the evaluation watermark was applied to every page."
    }
  ],
  "timings": {                                  // wall-clock, not a determinism guarantee
    "parseMs": 118.4,
    "layoutMs": 96.2,
    "renderMs": 0,
    "totalMs": 231.7
  }
}
```

## Validating a report yourself

```bash
curl -sO https://rendlio.com/schemas/compat-report/v1.json
ajv validate --spec=draft2020 -s v1.json -d report.json
```

The schema is served at its own `$id`, so that URL is fetchable rather than merely canonical. Within v1 only optional fields are ever added, so a validator pinned to v1 keeps working across engine updates.