Every supporting extractor runs, and canonical fields are namespaced per source
Context
Section titled “Context”The metadata extract job picked its extractor like this:
for _, e := range h.extractors { if e.Supports(mimeType) { ext = e; break }}EXIF, IPTC and XMP all answer yes for image/jpeg, and EXIF is registered
first. So on every JPEG ever uploaded, only the EXIF extractor ran. The
IPTC and XMP extractors — their parsers, their carrier walkers, their
twenty-odd CanonicalFields — could not be reached from production at all.
They were exercised solely by their own unit tests, which is why nothing said
so. Four of the seed dataset’s images carry a rights statement in XMP and
nothing in EXIF: the files that most needed reading were exactly the files
nothing read.
This surfaced while wiring the shipped field catalogue (#800): a field wired
to iptc_credit or xmp_rights would have routed nothing, silently — no
failure row, no log line. The wiring work was structurally impossible without
changing the dispatch.
The three namespaces are complements, not alternatives. EXIF describes the camera, IPTC the newsroom, XMP the rights; a photograph routinely carries all three, saying different things.
Decision
Section titled “Decision”1. Fan out. Every extractor whose Supports() answers yes for the MIME
type runs. The source is buffered once (each extractor ReadAlls its reader
anyway, so peak memory matches the old single-extractor path and storage is
read once instead of N times). One extractor failing hard does not stop the
others; an unknown error class aborts for the job framework’s retry. “No
extractor succeeded” is distinguished from “the file has no metadata” so
counter classification is preserved.
2. Canonical fields are namespaced per source, and that is the design, not
an accident. capture_datetime is EXIF’s, iptc_credit is IPTC’s,
xmp_rights is XMP’s. Two extractors’ views of the same semantic (an EXIF
Copyright and an XMP dc:rights) are different canonical fields, and they are
reconciled by the operator, one level up, by choosing which canonical a
field definition’s extraction_source names. The merge never decides which
namespace’s truth wins a semantic argument.
3. Merge precedence is a determinism guarantee, not a policy. Results
fold first-writer-wins in extractor registration order (EXIF, IPTC, XMP). In
practice nothing collides, because of §2. If a collision ever fires, the
right fix is a distinct CanonicalField, not a cleverer merge.
4. Provenance is carried per field. After a merge the result is no longer
one extractor’s word, so set_by records the extractor that actually
produced each value (exif/iptc/xmp/pdf/raw), with extract as the
honest fallback for results assembled without provenance. This is what makes
ADR 0012’s provenance model and ADR 0081 §3’s precedence
(extracted > default > empty, never over a chosen value) auditable.
Consequences
Section titled “Consequences”- Wiring a field to an IPTC or XMP canonical now routes. Before this, such a wire was a phantom control — configured, displayed in the admin, and dead.
- Every JPEG upload runs up to three parsers instead of one. They are in-memory parsers over an already-buffered source; storage is still read once. No I/O is multiplied.
- A new extractor added to the registry runs alongside the others by default. The registration-order comment (“EXIF first — largest catalog”) now describes merge determinism rather than a dispatch winner.
- The unreal-fixture lesson (ADR 0068) gains its largest instance yet: two entire extractors were green under test and unreachable from production. A subsystem’s tests passing says nothing about whether the subsystem is ever invoked — reachability needs its own assertion, which the wiring tests now provide end to end.