Skip to content
VERASPEC
Repository
Migration guidestable

§3 Producer checklist — 1.0.0 → 1.0.1

Work through these in order. Steps 1–3 are the only ways a Record that the 1.0.0 schema accepted can fail the 1.0.1 schema; steps 4–7 are behaviours the annex pinned that a schema cannot check.

3.1 Revalidate your corpus under the 1.0.1 schema

bash
PYTHONPATH=packages/ver-validator/src .venv/bin/python -m ver_validator.cli \
--schema 1.0.1 --format json conformance/valid/*.json

(Point it at your own corpus in place of conformance/valid/*.json.)

Anything that now fails, failed 1.0.0's prose already. Three defects can newly surface, and they are reported through four (code, pointer) pairs, because a wrong-width perceptual digest is caught twice — once by the schema stage, once by the digest stage. One defect, two codes; fix it once.

DefectReported asFix
pdq/1.0 digest is not exactly 64 lowercase hex charactersVER801 at /identity/perceptual/N/value, and VER201 at the same pointer (schema delta 1 made the pattern algorithm-conditional)§3.2
phash-dct/64 digest is not exactly 16 lowercase hex charactersVER802 at /identity/perceptual/N/value, and VER201 at the same pointer§3.2
An undeclared member inside a now-closed identity objectVER201 at /identity/content_hash or /identity/pixel_hashRemove it. There is no extension point inside identity in 1.0; record the datum outside the Record until 1.1's extensions lands

Those are the only ways a Record the 1.0.0 schema accepted can newly fail the 1.0.1 schema — the 1.0.1 delta is exactly three constraint changes. Other codes in the report are profile findings, not new-in-1.0.1 schema failures: they were already true of the Record under 1.0.0 and the profile validator is simply the first thing to check them. VER1302 (a raw segment whose sha256 does not match its own bytes) is the one you are most likely to meet; §3.3 covers it. The frozen 1.0.0 example demonstrates all of this at once:

bash
PYTHONPATH=packages/ver-validator/src .venv/bin/python -m ver_validator.cli \
--schema 1.0.1 standards/ver/1.0.0/example-record.json

3.2 Regenerate malformed perceptual digests

§5 always defined pdq/1.0 as 256-bit (64 hex) and phash-dct/64 as 64-bit (16 hex). The schema never enforced it, so producers shipped values that were too long, too short, uppercase, or algorithm-prefixed.

Three real failure modes, each with a different fix:

  1. Algorithm prefix in the value. A value like pdq/1.0:f8f8f0ce… puts the algorithm identifier inside the digest field. The alg field carries the algorithm; value carries only the digest. Strip the prefix — the digest itself is intact and needs no recomputation (finding HASH-01).
  2. Wrong width. If the value is not a recoverable digest, recompute it from the Canonical Buffer. Do not truncate a hash you cannot account for. The one truncation blessed in 1.0.1 is the golden example's, where the 96-hex value was provably the 64-hex digest with its own first 32 characters appended (value[:32] == value[-32:]), so value[:64] recovers it rather than inventing it.
  3. Uppercase hex. Lowercase it. Annex E22 pins all digests to lowercase hex, and the schema patterns are [a-f0-9].

Regenerating a perceptual digest does not change pixel_hash, content_hash, or any embedding. It is a metadata repair, not a re-ingest.

3.3 Verify raw-segment digests

metadata.raw[].sha256 must be the SHA-256 of the exact bytes that metadata.raw[].bytes base64-encodes, and those bytes must be the segment as stored in the container. Two independent things can be wrong:

bash
# self-consistency: does the digest match the payload you shipped?
# VER1302 flags every mismatch. Substitute your own corpus for the fixture path.
PYTHONPATH=packages/ver-validator/src .venv/bin/python -m ver_validator.cli \
--schema 1.0.1 conformance/valid/l1-raw-metadata.json

VER1302 catches the first failure. The second — bytes that are self-consistent but are not the container's stored bytes — no validator can catch, because the source asset is not in the Record. Annex E11 is the normative per-container table — read it there rather than from a summary, because it is the one place the byte ranges are stated and it covers containers this guide does not enumerate. Three consequences to check your extractor against:

  • One raw[] entry per stored segment, in file order. A preserved segment is the bytes as stored — for JPEG, the APP payload including its identifier prefix and any sequence octets; for PNG, the full chunk bytes, undecompressed. Do not concatenate a multi-segment family into one entry, and do not reassemble before hashing. E11's reassembly guidance is informative and applies to reading a family, never to what you store.
  • Byte-exact means bytes-as-stored. If your extractor re-frames, re-wraps, or normalizes a segment before hashing, both the bytes and the digest are wrong even though they agree with each other (finding REC-03).
  • Not every family is byte-exactly preservable in every container. Where E11 says a byte range is undefined for a container — TIFF's container-native EXIF IFD subtree is the case that bites — report the family through availability and rely on content_hash for the asset bytes. Do not invent a serialization and claim it is the stored segment.

Re-extract from the source assets if you still hold them; if you do not, the honest options are to drop the raw segment and set the family's availability to unknown, or to keep it and stop claiming L1+.

Also check annex E25 while you are here: image.source_bytes_ref.sha256 MUST equal identity.content_hash.value (VER1305). They always referred to the same bytes.

3.4 Confirm E3 — the quantize step is a scale, not a clip

This is the only correction that can move a pixel_hash, and it affects only sources deeper than 8 bits per channel (routine in TIFF and PNG, both in scope).

  • Correct behaviour: scale from the source bit depth to 8-bit, round_half_up(v * 255 / maxval), then clamp. Ties round toward +∞ on non-negative values.
  • The literal 1.0.0 reading: clamp 16-bit values to 255, which turns every sample above 255 into pure white.

If your producer scales — and every interoperable one does — nothing changes, and you have no re-ingest to do. If it clips, you have been emitting destroyed Canonical Buffers for >8-bit sources, and those Records must be re-ingested from source. Check with a synthetic 16-bit test image before assuming either way.

3.5 Confirm E6 — orientation source and out-of-range values

  • Orientation comes from EXIF/TIFF tag 274 in the primary EXIF IFD only.
  • XMP tiff:Orientation is reconciliation-layer data. CPNP-1 never consumes it. If your pipeline reads it, you are producing a different Canonical Buffer from every conformant producer for assets where the two disagree — a real case, since Adobe tooling writes both.
  • Out-of-range, zero, or malformed orientation values MUST be treated as 1 (upright), not rejected and not guessed.

If you consumed XMP orientation, re-ingest the affected assets: their pixel_hash values are wrong relative to the standard.

3.6 Confirm E2 — which dimensions you recorded

image.width/height are Canonical Buffer dimensions, i.e. post-orientation, and identity.pixel_hash.width/height MUST equal them. For Orientation 5–8 (the 90°/270° transposes) a producer that recorded stored dimensions has them swapped. Fixing this is a metadata repair — the buffer and its hash are unaffected, since the hash preimage already carried the post-orientation dimensions.

3.7 Check the rulings that make records non-conformant without changing bytes

Run the profile at your claimed level and read the errors:

bash
PYTHONPATH=packages/ver-validator/src .venv/bin/python -m ver_validator.cli \
--level auto conformance/valid/*.json

The rulings most likely to surface in an existing corpus:

RulingCodeWhat to do
E18 — space_id unique within a RecordVER401Deduplicate spaces[]. Two descriptors under one id is a contradiction, not a merge
E24 — dim/dtype must equal the descriptor'sVER601, VER602Fix whichever is wrong. If you omit them, the descriptor's values apply (VER606 warns)
E20 — recipe.inputs are space_idsVER704Replace role names with the space_ids declared in the same Record
E15 — no redaction claim over preserved evidenceVER1005Omit the raw segment from raw[], or stop claiming the family is redacted. Scrubbing the segment in place is not an option in 1.0.x — §5 covers the two-envelope pattern
E28 — C2PA state consistencyVER1103availability.c2pa: "present" with status: "missing" is a contradiction. Report what you actually checked
E13 — a descriptor without reference backs no conformance claimVER404 (warning)Publish a reference set and tolerance, or stop presenting the space as reproducible
E13 — a descriptor whose preprocessing omits the §6.1 members for its kindVER405 (warning)State the per-kind members in full: visual spaces need resize, interpolation, antialias, crop, mean, std, channel_order; text spaces need tokenizer, tokenizer_sha256, max_length, truncation, casing
§7.2 — a stripped_suspected family needs a basisVER901State the basis, or use unknown

3.8 Adopt the definitions you were previously free to invent

No code change is needed unless you disagreed with the pinned reading. Where you did, the cost differs by item: E7, E8 and E9 sit on the CPNP side, so a producer on the non-selected reading has been emitting Canonical Buffers no other implementation reproduces — those pixel_hash values are withdrawn and the affected assets need re-ingesting, exactly as for E3 (§3.4) and E6 (§3.5). The rest are metadata definitions: adopting them costs a repair, never a re-ingest.

  • E16record_id is unique per (producer, record), UUIDv5 RECOMMENDED over a producer-scoped stable identity. Deriving it from pixel_hash alone collides across distinct Records of the same asset (finding IMPL-RECORD-ID-COLLIDES). created_at is record assembly time and SHOULD be at or after every chain timestamp; the chain SHOULD be chronological (VER1201VER1204).
  • E17 — a Record signature is computed over the Record with provenance.signature removed, RFC 8785 JCS-canonicalized, as a detached JWS or COSE. A signature over any other payload is unverifiable by anyone else.
  • E19 — grade is derived from source.class; there is no grade field and none is coming in 1.0. Consumers satisfy §8's filtering MUST through the mapping table.
  • E8 — a present-but-unusable ICC profile fails closed. Reject, or emit a diagnostic state outside the Record. Silently assuming sRGB and emitting a conformant Record is prohibited, and an undeclared icc_error field was never legal in a closed schema.
  • E9 — only an embedded ICC profile is consumed. gAMA, cHRM, sRGB chunks and EXIF ColorSpace are not.
  • E7 — the first frame is the decoder's frame at logical index 0, fully coalesced; animated is true iff frame count > 1.
  • E12 — if your decoder does not pass a pinned implementation profile against a reference corpus, your results are PROVISIONAL. Say so rather than claiming conformance you cannot demonstrate. The corpus itself is a 1.1 deliverable.

3.9 Re-point your schema reference

diff
- "$schema": "https://verstandard.org/schemas/ver-record-1.0.json"
+ "$schema": "https://verstandard.org/schemas/ver-record-1.0.1.json"

If you validate against a vendored copy, take standards/ver/1.0.1/ver-record.schema.json. Do not overwrite your vendored 1.0.0 copy — a published $id is never reused, and keeping both lets you diff reports across releases.

Do not touch ver_version. It stays "1.0". Bumping it to "1.0.1" makes a Record that no consumer accepts: the field is a const in both schemas.