Compatibility report
Every conversion (and every diagnose) emits one JSON report answering: what was in this workbook, what did Rendlio do with each thing, and what should the user check? It enforces the engine-wide rule: nothing is ever silently approximated — every deviation from faithful rendering is a report entry with a location.
The report is a public, versioned API. You get it with --report <path> (or --report - for a stderr summary only); programmatic hosts should always set it.
Schema and versioning
Section titled “Schema and versioning”- Canonical machine schema (JSON Schema 2020-12):
https://rendlio.com/schemas/compat-report/v1.json— the single source of truth; this page is written from it. schemaVersionis semver-like"1.x": additive optional fields bump the minor version; anything else bumps the major version (and gets a new schema URL). v1.0 freezes at the first milestone release.- The envelope is strict:
additionalProperties: falseat every level, so a report never carries undocumented fields.
Envelope fields
Section titled “Envelope fields”All top-level fields are required unless marked ?.
| Field | Type | Meaning |
|---|---|---|
schemaVersion | string, 1.x | Report schema version |
engine | object | name (always "Rendlio"), version, releaseDate (ISO date of the build) |
input | object | sha256 (64 hex chars of the input bytes), sizeBytes, fileName? (omitted for stdin) |
options | object | 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 |
mode | "convert" | "diagnose" | Which mode produced the report |
result | object | status (see derivation) and exitCode (0–5) |
pages | object | count — rendered page count; 0 for diagnose and for failures |
sheets | array | One entry per sheet: name, index, printed (false = hidden or outside the print area) |
features | array | The heart of the report — see feature entries |
fonts | object | substitutions (each requested → used with a reason: metric-substitute | fallback | missing-default) and missing (requested families with no substitute rule) |
formulas | object | cellsTotal, cachedMissing (formula cells with no cached value), recalcMode (off | strict-v1), closureSize (integer when strict-v1 evaluated/refused, else null) |
warnings | array | Coded warnings — see warnings |
timings | object | parseMs, layoutMs, renderMs, totalMs |
Feature entries
Section titled “Feature entries”Each entry in features[]:
| Field | Type | Meaning |
|---|---|---|
id | string | Stable feature identifier, pattern F-<AREA>-<slug> where <AREA> is one of PARSE, STY, NUM, LAY, CF, CHT, IMG, TXT, PDF, PNG, REC, LIC, SEC |
status | enum | rendered | approximated | unsupported | skipped — see below |
count | integer ≥ 1 | Occurrences of this (feature, status) pair |
locations? | array, ≤ 20 | Where — see locations |
locationsTruncated? | boolean | true when more than 20 locations existed |
detail? | string | Plain-language statement of the rule that applied |
Status semantics (normative)
Section titled “Status semantics (normative)”| Status | Meaning | Example |
|---|---|---|
rendered | Faithful per the specification rule for this feature | merged cell drawn as specified |
approximated | Rendered, but a documented deviation applies — the detail says which | timePeriod conditional format evaluated at conversion date |
unsupported | Present in the file, not rendered; the space it would occupy follows the specified rule for that feature (usually rendered empty) | a chart type outside the supported set |
skipped | Present but intentionally outside conversion semantics (not a fidelity gap) | VBA project (never executed), data validation rules |
Reporting rule: rendered entries are recorded only in diagnose mode — convert-mode reports list only deviations, keeping them small. approximated / unsupported / skipped always appear in both modes. So in a convert report, an empty features array means nothing deviated.
Locations
Section titled “Locations”A location is { "sheet": string?, "ref": string?, "objectId": string? } with at least one field non-null. ref is an A1 cell or range ("B7", "A1:C30"); objectId is the drawing/chart part-relative id. Per feature entry, at most 20 locations are listed; beyond that, locationsTruncated is true while count keeps the real total. Output ordering is deterministic — identical runs produce byte-identical reports.
Result status & exit-code derivation
Section titled “Result status & exit-code derivation”The report’s verdict and the process exit code are derived by one ladder (normative):
if internal bug → error / 5elif resource limit hit → resource-limit / 4 (outputs deleted)elif input invalid (not xlsx, encrypted) → invalid-input / 3elif any feature status == unsupported → unsupported-content / 2elif any warning OR any approximated → warnings / 1else → success / 0diagnose uses the same ladder but never writes documents; exit 2 there means “this file contains content Rendlio would not fully render”.
Warnings and W-code ranges
Section titled “Warnings and W-code ranges”Each entry in warnings[]: code (pattern W + 4 digits), severity (info | warn | error), message (static template text — never cell contents), and an optional location. Codes are owned by engine modules in fixed ranges:
| Range | Domain |
|---|---|
| W1xxx | parse / OPC / ZIP |
| W2xxx | styles (W2001–W2099), number formats (W2100–W2199) |
| W25xx | conditional formatting |
| W3xxx | layout / print |
| W4xxx | charts / drawings / images |
| W5xxx | fonts / text |
| W6xxx | formulas / recalculation |
| W7xxx | PDF / PNG output |
| W8xxx | limits / security |
| W9xxx | licensing (e.g. W9001 — no licence found, evaluation watermark applied) |
Annotated example
Section titled “Annotated example”Real reports are plain JSON; comments here are annotation only.
{ "schemaVersion": "1.0", "engine": { "name": "Rendlio", "version": "0.3.1", "releaseDate": "2026-10-02" }, "input": { "sha256": "…64 hex…", "sizeBytes": 183422, "fileName": "invoice.xlsx" }, // fileName is omitted when the input came from stdin
"options": { /* exact echo of effective CLI options, incl. defaults and licenseMode */ }, "mode": "convert", // "convert" | "diagnose"
"result": { "status": "warnings", // the ladder's verdict … "exitCode": 1 // … and the process exit code, always in agreement },
"pages": { "count": 7 }, // 0 for diagnose / failures "sheets": [ { "name": "Summary", "index": 0, "printed": true }, { "name": "Data", "index": 1, "printed": false } // hidden / outside print area ],
// Convert mode: deviations only. An empty array = fully faithful. "features": [ { "id": "F-CHT-stacked", "status": "unsupported", // rendered | approximated | unsupported | skipped "count": 2, "locations": [ { "sheet": "Summary", "objectId": "chart2" } ], "locationsTruncated": false, "detail": "Stacked bar charts are not yet rendered (planned post-GA)." } ],
"fonts": { "substitutions": [ { "requested": "Calibri", "used": "Carlito", "reason": "metric-substitute" }, { "requested": "Aptos Narrow", "used": "Carlito", "reason": "missing-default" } ], "missing": [ "Aptos Narrow" ] },
"formulas": { "cellsTotal": 1420, "cachedMissing": 12, // formula cells with no cached value to render "recalcMode": "off", // off | strict-v1 "closureSize": null // integer when strict-v1 evaluated/refused },
"warnings": [ { "code": "W5001", "severity": "warn", // info | warn | error "message": "Default font 'Aptos Narrow' is not available; substituted 'Carlito'. Column widths and line breaks may shift.", "location": { "sheet": null, "ref": null, "objectId": null } } ],
"timings": { "parseMs": 41, "layoutMs": 118, "renderMs": 96, "totalMs": 262 }}Diagnose mode & privacy
Section titled “Diagnose mode & privacy”rendlio diagnose produces the same schema with mode: "diagnose" and pages.count: 0, and must not include cell contents: detail strings and message templates are static text plus counts and locations only. CI includes a leak test — fixtures with sentinel strings in cells assert the sentinels never appear in diagnose output. You can safely run diagnose on confidential workbooks and share the report with support.
Gating CI on the report
Section titled “Gating CI on the report”Level 1 — exit codes only
Section titled “Level 1 — exit codes only”The exit code alone is a complete gate (0 clean, 1 warnings, 2 unsupported content, 3/4/5 failures):
rendlio convert workbook.xlsx -o out.pdf --format pdfa --report report.jsoncode=$?if [ "$code" -gt 1 ]; then echo "conversion not clean (exit $code) — see report.json" >&2 exit "$code"fiTypical policy: treat <= 1 as pass and 2 as review. On exit 4 partial outputs are deleted — you never gate on a truncated document — and the report is still written, naming the limit that fired.
Level 2 — jq assertions on the report
Section titled “Level 2 — jq assertions on the report”Fail unless the run was fully clean:
jq -e '.result.status == "success"' report.jsonFail on any unsupported content, but print what it was first:
jq -r '.features[] | select(.status == "unsupported") | "\(.id) ×\(.count): \(.detail // "see docs")"' report.jsonjq -e '[.features[] | select(.status == "unsupported")] | length == 0' report.jsonFail on font substitutions (layout may shift when fonts substitute):
jq -e '.fonts.substitutions | length == 0' report.jsonAllow specific warning codes, fail on anything else:
jq -e --argjson allowed '["W9001"]' \ '[.warnings[].code] - $allowed | length == 0' report.jsonFail when formula cells are missing cached values (they render empty in off mode):
jq -e '.formulas.cachedMissing == 0' report.jsonLevel 3 — schema validation
Section titled “Level 3 — schema validation”Validate the report itself against the published schema (for example with ajv):
ajv validate -s compat-report.v1.schema.json -d report.jsonBecause schemaVersion only gains optional fields within v1, a validator pinned to v1 keeps working across engine updates.