Skip to content

JSON report

wacz-validator-validate prints one JSON object to stdout. That is its only output format — there is no flag to switch (to read the same report interactively, use the separate wacz-validator binary from @wacz-validator/tui). It is the interface for CI scripts and for anything downstream of wacz-validator, so its shape is deliberate rather than incidental.

Terminal window
wacz-validator-validate samples/wikipedia.wacz | jq '.summary'
export interface Report {
/**
* この report を作った build の版。CLI と daemon は、build したときに git のタグから
* 決めた版を渡す —— タグの上なら `0.31.0`、その先なら `0.31.0+3.gabcdef1`、未コミットの
* 変更があれば `dirty` が付き、タグが見えなければ `unknown`。
*/
validatorVersion: string;
/** report を評価した profile。{@link ReportProfile} を参照。 */
profile: ReportProfile;
/** validate された WACZ の identity。{@link ReportSource} を参照。 */
source: ReportSource;
summary: ReportSummary;
issues: Issue[];
/** WACZ 内のファイル一覧 + 検証の紐付け。{@link ReportEntry} を参照。 */
entries: ReportEntry[];
/** best-effort な metadata — {@link ReportStats} を参照。 */
stats?: ReportStats;
/**
* producer のバージョンが合わず走らせなかった rule。**該当が無ければ key ごと
* 出ない** — {@link SkippedRule} を参照。
*/
skipped?: readonly SkippedRule[];
}

“Is this archive usable?” is summary.failed === 0. That answer used to be cached in a separate field; it only created room to disagree with summary, so it is gone.

Terminal window
wacz-validator-validate a.wacz | jq '.summary.failed == 0'

issues are in rule registration order, which means the structural checks come first and the most likely producer bug tends to be near the top.

It takes one of three shapes.

kind field example
file path /var/archives/a.wacz (an absolute path)
s3 uri s3://bucket/key.wacz
http url https://example.com/a.wacz

The http form has its query stripped. When a signed URL (?X-Amz-Signature=…) was validated, leaving that signature in the report would let anyone who can read the report read the archive until it expires. A signature is a credential, not an answer to “which archive is this” — the same reason s3 carries no connection settings (forcePathStyle) in its uri.

export interface ReportSummary {
passed: number;
failed: number;
warnings: number;
info: number;
durationMs: number;
}
/**
* report を評価した profile。engine の `ProfileSelector` を wire 向けに写す。
*
* **バージョンは文字列で出す。** engine 内部の `SemVer` は `{ major, minor, patch }`
* の三つ組だが、あれは parse の中間結果であって、報告を読む人が欲しいのは
* `"2.1.0"` のほう。同じ report にある {@link SkippedRule} の `range`
* (`">=1.11.0"`) とも並びが揃う。
*
* `version` は**操作者が名乗った producer のバージョン**で、archive と照合した
* ものではない (`datapackage.json` の `software` は読んでいない)。
*/
export interface ReportProfile {
name: RuleProfile;
/** 未指定なら **key ごと出ない**。「バージョンを問わない」の意。 */
version?: string;
}
/**
* producer のバージョンが合わずに走らせなかった rule。
*
* `Report.skipped` は該当が 1 件も無ければ **key ごと出力されない**
* (`stats` と同じ条件付き spread)。だからバージョンを指定しない実行の JSON は
* 従来と 1 バイトも変わらない。
*/
export interface SkippedRule {
rule: string;
reason: "profile-version";
/** rule が要求した範囲 (例 `">=1.11.0"`)。 */
range: string;
// 名乗られたバージョンはここには書かない。全エントリで同じ値になる複製で、
// `Report.profile.version` から機械的に取れる。
}

durationMs measures the validation run, not process startup.

Each issue names the rule that raised it, so a report can be filtered or diffed by rule without parsing prose:

  • rule — the stable <area>/<short-name> identifier, the same string used in the Rules table. Never localised, never reformatted between versions.
  • severity — after profile re-grading, so this is what the run actually decided rather than the rule’s default.
  • messageKey + params — the message is not pre-rendered prose. core keeps an i18n key and the runtime values; the renderer resolves them against a locale catalogue. That is what lets the TUI and a future browser frontend present the same report in different languages.
  • location — where in the archive, when the rule can say.
  • details — rule-specific extras: a hash diff, a hex dump. Deliberately untyped; a renderer formats it per rule.
  • docs — links to the clause in the source specification, so a reader can go from a finding to the text that motivates it.
  • conformance — how strongly the specification demands it: MUST, MUST NOT, SHOULD, SHOULD NOT, MAY. Resolved from the rule name rather than written by the rule, so the rule definition stays the single source.

severity and conformance answer different questions

Section titled “severity and conformance answer different questions”

severity is what wacz-validator does about a violation; conformance is what the specification asks for. Filtering on one is not a substitute for the other.

Across the 30 corpus specimens:

conformance error warning info
MUST 20 10 0
SHOULD 1 8 0
MAY / MUST NOT 0 4 1

The two bold cells are why neither column predicts the other. datapackage/resources-complete is a MUST reported as a warning — an undeclared file in the ZIP does not stop replay. datapackage/digest runs the other way: the spec only says SHOULD, but a hash that does not match may mean the archive was altered, so wacz-validator calls it an error.

Note that summary.failed counts severity alone — it never looks at conformance. An archive can have failed: 0 and still violate a MUST. If you need the conformance view, read it off the issues:

Terminal window
# What wacz-validator judges to be broken
wacz-validator archive.wacz | jq '[.issues[] | select(.severity == "error")]'
# What the specification requires
wacz-validator archive.wacz | jq '[.issues[] | select(.conformance == "MUST" or .conformance == "MUST NOT")]'
# Where the two disagree — usually the most informative list
wacz-validator archive.wacz | jq '[.issues[]
| select((.conformance == "MUST" and .severity != "error")
or (.conformance != "MUST" and .severity == "error"))]'

There is no CLI flag for this yet; the report carries both axes and the filtering is left to the caller.

validatorVersion exists so a consumer can detect schema drift rather than guess at it. It is the release the build came from, read from the git tags when it was built:

validatorVersion The build was
0.31.0 the tagged commit, with no uncommitted changes — the release itself
0.31.0+3.gabcdef1 three commits past v0.31.0 (abcdef1)
0.31.0+dirty, 0.31.0+3.gabcdef1.dirty the same, plus uncommitted changes
unknown made where no tag was visible (a shallow clone)

Only a plain X.Y.Z is a release. The rest carries SemVer build metadata (+…) rather than a prerelease (-…), which would read as before the release.

Reports from builds before 0.31.0 say 0.0.0, whatever they were built from: the field then mirrored package.json#version, which never moves here — the tags are the only version.

Within a major version:

  • Existing fields keep their meaning and type.
  • rule identifiers are stable — they are the thing to key on.
  • New optional fields may appear. Consumers should ignore what they do not know rather than reject the document.

stats is best-effort and optional by design: a WARC broken enough to defeat statistics must not block the report that says so.

profile — the conditions the report was produced under

Section titled “profile — the conditions the report was produced under”

profile is an object with a name and an optional version. Without a pinned version the version key is absent, which is what “any version” looks like.

"profile": { "name": "browserhive", "version": "2.1.0" }
"profile": { "name": "spec" }

It is an object rather than a string so that name alone is enough to match on. A single string like "browserhive@2.1.0" breaks every equality check the moment someone pins a version.

Terminal window
jq -r '.profile.name' # "browserhive", pinned or not
jq -r '.profile.version // "unpinned"' # when only the version matters

version is what --profile claimed, not something checked against the archive — see Profiles.

skipped — recording what was not looked at

Section titled “skipped — recording what was not looked at”

Append a producer version to --profile (browserhive@1.10.0) and any rule that cannot judge that version correctly does not run. That omission is written down, in skipped:

"skipped": [
{
"rule": "warc/recording-complete",
"reason": "profile-version",
"range": ">=1.11.0"
}
]

Like stats, the key is absent when there is nothing to report, so a run without a version produces byte-identical JSON to before.

It is explicit rather than omitted because an empty issues has two possible meanings — “looked, found nothing” and “never looked”. A consumer that assumes the first is being misled.

The exit code is derived from the report by exitCodeFor in @wacz-validator/protocol, shared by the CLI and the TUI so the two cannot disagree. Deciding from summary.failed in your own script is equivalent for the common case; use the shared helper when the distinction between failure kinds matters.