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.
wacz-validator-validate samples/wikipedia.wacz | jq '.summary'Top level
Section titled “Top level”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.
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.
source — which archive this was
Section titled “source — which archive this was”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.
Summary
Section titled “Summary”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.
Issues
Section titled “Issues”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:
# What wacz-validator judges to be brokenwacz-validator archive.wacz | jq '[.issues[] | select(.severity == "error")]'
# What the specification requireswacz-validator archive.wacz | jq '[.issues[] | select(.conformance == "MUST" or .conformance == "MUST NOT")]'
# Where the two disagree — usually the most informative listwacz-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.
What is promised
Section titled “What is promised”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.
ruleidentifiers 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.
jq -r '.profile.name' # "browserhive", pinned or notjq -r '.profile.version // "unpinned"' # when only the version mattersversion 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.
Exit codes
Section titled “Exit codes”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.