veripublica machine-output format

Version 0.4.1.

Implemented. epubveri v0.5.0 (2026-07-11) shipped the first --format json; epubsana v0.2.0 followed. The envelope is an observed contract: from those releases on, its shape is stable within the convention’s stability boundary (CLI.md §9) — the guarantee this document carried as a promise while it was provisional (v0.1.0–v0.4.0).

The json format name is reserved by CLI.md §3 for every tool: one that has not implemented it rejects --format json rather than falling back to human.

This document specifies the JSON a tool emits under --format json, so one veripublica tool (or any external program — Sigil, Calibre, a CI job) can consume another’s output without bespoke parsing.

human format is for people and MAY change freely. json is a contract: its shape is stable within the convention’s stability boundary (CLI.md §9).


1. The envelope

Every --format json invocation that runs prints exactly one JSON object to stdout. A usage error produces no envelope: per CLI.md §5 it is short stderr text with exit 2, and stdout stays empty. The envelope describes runs that happened; inputs that could not be processed are described inside it.

{
  "tool": "epubveri",
  "tool_version": "0.5.0",
  "convention": "0.4",
  "status": "error",
  "dry_run": false,
  "inputs": [
    {
      "path": "a.epub",
      "status": "ok",
      "summary": { "errors": 0, "warnings": 0 },
      "items": []
    },
    {
      "path": "b.epub",
      "status": "problems",
      "summary": { "errors": 3, "warnings": 1 },
      "items": [
        {
          "type": "finding",
          "code": "RSC-005",
          "severity": "error",
          "location": "OEBPS/ch1.xhtml",
          "position": { "line": 44, "column": 9 },
          "message": "Error while parsing file: element \"img\" missing required attribute \"src\""
        }
      ]
    },
    {
      "path": "c.epub",
      "status": "error",
      "error": "cannot read: not a ZIP archive",
      "items": []
    }
  ]
}

This run exits 2 (at least one input could not be processed, CLI.md §6) — and still carries the full reports for a.epub and b.epub: the process-every-input rule, in JSON form.

A transformer’s input object additionally reports what it wrote — the same fact its human output already prints (wrote book_fixed.epub):

{
  "path": "book.epub",
  "status": "ok",
  "output": "book_fixed.epub",
  "summary": { "errors_before": 150, "errors_after": 0, "applied": 2, "skipped": 0 },
  "items": [
    {
      "type": "fix",
      "outcome": "applied",
      "code": "RSC-016",
      "rule": "htm.entity.undeclared",
      "severity": "error",
      "location": "OEBPS/ch1.xhtml",
      "message": "Mapped 774 undeclared HTML entities to characters",
      "data": { "occurrences": 774 }
    }
  ]
}

1.1 Envelope fields

Field Type Meaning
tool string The tool’s name (e.g. "epubveri").
tool_version string The tool’s SemVer.
convention string The convention’s stability key: the version prefix at which stability is guaranteed — "0.4" while the convention is 0.x, "1" from 1.0.0 on (CLI.md §9). Compare with string equality; there is nothing finer to parse.
status string Mirror of the exit code, aggregated over the inputs: "ok" (every input clean / every goal met) → 0; "problems" (every input processed; error- or fatal-severity findings, or an unmet goal, remain — CLI.md §6’s threshold) → 1; "error" (at least one input could not be processed) → 2. A tool that could not run at all emits no envelope.
dry_run boolean true when the run was --dry-run: the identical shape, items describing what would be done (every item’s outcome is "proposed"), output naming what would be written. Absent means false. The flag is a summary of the items; the two can never disagree.
summary object Optional aggregate counts (small, flat, tool-specific). Derivable from the inputs; a consumer MUST NOT require it.
inputs array One input object per -i, in command-line order — an array even when there is exactly one.

1.2 Input object fields

Each element of inputs is self-contained — deliberately, so that other surfaces of the same engine (a wasm binding’s return value, for instance) can reuse the shape as-is.

Field Type Meaning
path string The input path as given on the command line.
status string "ok", "problems", or "error" — this input alone.
error string Present only with status: "error": why this input could not be processed. (Named error, not messagemessage already means something at the item layer.)
output string Transformers only: the path written — or, under dry_run, the path that would be written. The report states the facts of the run; what a consumer does with the path is not this specification’s business.
summary object Tool-specific counts for this input (small, flat). Optional.
items array The findings / fixes / operations for this input. May be empty.

status: "error" means no report was possible — it never grades a verdict. A defect the tool can still name with a code, however severe (a fatal-severity finding included), is a verdict: the input is "problems", and the finding is in items, where a consumer — a repairer above all — can plan against it.

1.3 Item fields

Each item is an object. These fields are shared — a consumer can rely on them across tools — and a tool MAY add more under data:

Field Type Meaning
type string Item kind: "finding" (verifier), "fix" (repairer), "operation" (transformer).
outcome string What happened to the item: "applied", "skipped", or "proposed". Required on "fix" and "operation" items; never present on "finding" items. See below.
code string The stable, tool-facing code — for EPUB tools, the epubcheck-compatible message ID (e.g. "RSC-005").
rule string Optional finer sub-code (e.g. "ncx.ids.invalid_ncname").
severity string One of "fatal", "error", "warning", "info", "usage" — epubcheck’s vocabulary, completing the code contract. See below.
location string Container-relative path the item concerns, if any.
position object { "line": N, "column": N } (1-indexed), if known.
message string Human-readable one-liner.
data object Tool-specific extra fields (counts, before/after values, …).

Fields that don’t apply MAY be omitted. Consumers MUST ignore unknown fields (so tools can extend data without breaking anyone).

severity is a reserved value set, not a requirement: a tool emits only the values it has a concept of — a non-EPUB tool that never has a usage finding simply never emits one. The set is closed: a new value enters by issue and release (CONTRIBUTING §2: no value is invented for an unnamed need), never through data. On a fix or operation item, severity is inherited — the severity of the finding the item addresses, verbatim from the detector — never how noteworthy the report line is. Severities below error never move the exit code (CLI.md §6). usage-severity items are always present in json: the envelope is for machines, which filter; suppression-by-default is a human-surface concern.

outcome carries “did, or would?” per item, because a consent-per-fix repairer routinely mixes both in one ordinary run. "applied" — the change was made. "skipped" — presented and not done: the caller declined. "proposed" — no decision exists yet: a dry run, or a consent that arrives after the report. Under dry_run: true every item is "proposed". A transformer that always applies everything stamps "applied" on every item — the field being required on fix/operation items is what keeps its absence from meaning anything. The value set is closed, like severity’s.

2. What is standard, and what is each tool’s

The skeleton is standard: the envelope fields, the input object, and the shared item fields above. Without them a consumer can rely on nothing.

The flesh is tool-owned: summary’s keys and items[].data’s contents are each tool’s own vocabulary, documented in that tool’s docs.

The bridge between the two is the same rule the CLI uses for option names (CONTRIBUTING §2): a data key is born in one tool; when a second tool needs the same key, it is promoted to a shared item field — by an issue on this repository, not by drift.

A reference implementation of the skeleton exists — non-normative: the JSON above is the contract, the types are a convenience. It lives in epubveri::envelope (the reference tool), generic over the two tool-owned slots (summary, data), and is used by epubveri and epubsana. It stays there until a veripublica tool that does not depend on epubveri needs the envelope, at which point it moves to its own crate — the promotion rule above, applied to implementation shapes (#27).

3. Guarantees

All of the above binds since epubveri v0.5.0 (2026-07-11), the first implementation — the moment the provisional period was defined to end. The document spent four versions provisional, in contract language, precisely so that hardening, when it came, was a decision rather than a rewrite.