```
run_id:        2026-09-10-viallo-camera-metadata-census
product:       viallo
tier:          standard
written_at:    2026-09-10T21:55:00Z
```

## 1. Question

**Of the JPEG photographs published on Wikimedia Commons, what share carries a camera serial
number, and what share carries a location coordinate — and how does each of those two shares differ
between camera makes and between camera models?**

The population is named: photographs published on Wikimedia Commons, as a random sample of its file
namespace. It is not "photographs in general" and the article may not say so.

## 2. Method

**Inputs and where they come from.** The file namespace of Wikimedia Commons, sampled with the
MediaWiki API's random generator, which is the platform's own random draw rather than a selection
made by this run:

```
https://commons.wikimedia.org/w/api.php?action=query&format=json
  &generator=random&grnnamespace=6&grnlimit=10
  &prop=imageinfo&iiprop=url|size|mime
```

Each call returns ten random file pages; the call repeats until 1 500 files have been accepted.

**Acceptance rule, fixed now.** A file is accepted when its `mime` is `image/jpeg`. Every rejected
file is written to `data/rejections.jsonl` with its title, its MIME type and the reading
`not-a-jpeg`. Nothing is dropped silently and no rejection is re-drawn to make a round number.

**Tool or command per step.**

1. `tools/sample.mjs` draws the random pages, applies the acceptance rule, and writes the accepted
   titles to `data/frame.jsonl`.
2. `tools/measure.mjs` fetches, for each accepted file, the first 131 072 bytes with a `Range`
   request to the URL the API returns, and parses that buffer with `tools/parse-head.mjs`. A
   `User-Agent` naming this study and its contact address is sent on every request.
3. 1 200 milliseconds of sleep between consecutive file requests. This is a measured setting, not a
   courtesy: ten requests without a pause drew HTTP 429 from Wikimedia, and twelve requests at this
   spacing drew none.
4. A whole-file validation subsample: every tenth accepted file is downloaded in full and parsed in
   full, and the two readings are compared field by field.

**Output file and its shape.** `data/units.jsonl`, one row per accepted file, appended the moment it
is produced, never rewritten. Fields: drawn-at timestamp, title, API-declared size and MIME, HTTP
status, bytes received, whether the range request was honoured, the parser's own `parse_error`,
`has_exif`, `make`, `model`, `make_is_consumer_device`, `serial_tag_present`, `gps_ifd_present`,
`gps_coord_tag_present`, `gps_coord_all_zero`, `datetime_original_present`, `software_present`,
`icc_present`, `xmp_present`, `width`, `height`, `head_window_cut`, `metadata_region_complete`, and
the instrument's version string.

**The instrument changes in exactly one way, and it is stated because it is a change.** The parser
inherited from the previous run reports presence only and never emits a value: no coordinate, no
serial number, no date, no software string. This study needs to group files by camera model, which
presence cannot do, so the instrument is extended to emit **the `make` and `model` strings and
nothing else that is a value.** Make and model are product identifiers that appear on the camera's
box; a coordinate and a serial number do not, and neither reaches the output, the dataset or the
article. The extension is written and calibrated before the first real file is fetched.

**What is discarded, and on what rule.** A file whose parse fails is kept as a row with its
`parse_error`, and reported as a parse failure. A row is discarded only when the instrument was
changed after it was written, and then only by re-fetching it — never by editing it. The discarded
rows are moved to `data/discarded/` so the history stays readable.

## 3. Expectation

Two directions, written before anything is fetched, both taken from the previous study on the same
platform (300 Commons originals: a camera serial number in 12.0 per cent, a location coordinate in
13.7 per cent).

- **Location:** between 8 and 25 per cent of accepted files carry a coordinate tag whose values are
  not all zero.
- **Serial number:** the interesting one. **I expect the share to be a property of the manufacturer
  rather than of the file.** Stated as something a script can evaluate: at least half of all
  serial-bearing files come from at most three makes, and at least one make with twenty or more
  accepted files sits at or below one per cent.

## 4. Falsifier

The expectation is wrong if **no make differs from the overall serial rate by more than five
percentage points** — that is a uniform field, not a manufacturer policy, and the article then
reports a uniform field. For the location arm the expectation is wrong if the share falls outside
8 to 25 per cent. Both outcomes are publishable and neither is edited into a shape that reads
better.

## 5. Abandonment condition

The run stops and records `NOT_MEASURABLE` if either holds after the first 200 accepted files:

- the parser returns a `parse_error` on more than a quarter of them, or
- Wikimedia answers HTTP 429 on more than 5 per cent of requests at 1 200 ms spacing.

## 6. Estimate

- **What the run needs from Jakub:** nothing. No account, no key, no install, no device. Every input
  is a public file fetched anonymously.
- Machine time: about 1.5 hours of fetching across the sample, plus the validation subsample;
  roughly 4 hours of wall clock with the agent sessions.
- Money: 0 USD. Nothing is bought and no third-party API is called.
- Claims expected in the ledger: descriptive shares (type M) for each field, a per-make comparison
  (type M), a per-model table (type M), and one type-S definition of the EXIF fields named.

## 7. Adversary dispositions

Filled after `ADVERSARY.md` returns and before Gate B renders.

## 8. Amendments

Append-only.

**A-6, 2026-09-10.** The header's `written_at` reads `2026-09-10T21:55:00Z`, which is the local clock
with a `Z` attached rather than UTC. This machine runs at UTC+7, so the true instant is
**2026-09-10T14:55:00Z**, and the same correction applies to the times quoted in D-01. The ordering
the reader needs is unaffected and is now checkable: the pre-registration at 14:55Z, the frame drawn
at 15:14:51Z, and the first measurement of this instrument at 16:14:12Z, all on 2026-09-10.

**A-7, 2026-09-10.** The frame was drawn to 3 000 accepted files, not the 1 500 named in section 2.
A coordinate appears in about one file in ten, so the per-make cells the question asks about need
the larger draw; the acceptance rule, the random generator and the pacing are unchanged, so this is
a larger random sample of the same population and not a different one. The whole-file subsample
stays at every tenth file, and the per-model table carries the models with at least twenty files,
a threshold stated with the table rather than chosen to fit it.

**A-5, 2026-09-10.** `serial_tag_present` is replaced by three fields: `body_serial_tag_present`
(EXIF 0xA431), `lens_serial_tag_present` (0xA435) and `xmp_serial_present` (`aux:SerialNumber`).
The single flag counted a lens serial as a camera serial and could not see an XMP serial, so it was
neither a superset nor a subset of what the platform reports. The byte arm restarts from row zero on
the corrected instrument, `census-2`; the rows from the first pass are archived rather than merged.

**A-1, 2026-09-10, before the full validation subsample was read.** The serial comparison groups
camera manufacturers and treats the phone makers as one family. Declared here rather than after the
counts are visible: on the first 87 whole-file validations no phone file carried a serial in any
group while Nikon carried ten of twelve, so a per-model comparison would report cells too small to
compare and the family is the unit the question is about. Every figure is still reported with its
raw count beside it.

**A-2, 2026-09-10.** The census instrument gained a `stream error` path. Measured the same day: a
transfer that dies mid-stream raises `Z_BUF_ERROR`, and the process had been exiting zero with the
counters in memory, so a dead run was indistinguishable from a finished one. The run over the local
file starts from zero for the third time; the counters were never mixed between starts.

**A-3, 2026-09-10.** Shares of files are published from the random frame only. `RECONCILE-INSTRUMENTS.md`
records why: the dump reader and the platform extractor agree file by file on 150 identical files,
so the twenty-point difference between their aggregates is composition, and the frame is the
instrument that describes the files. Census figures enter the article as structure between cells of
one instrument, never as a level.

**A-4, 2026-09-10.** The validation subsample stays at every tenth file for this pass. If a compared
cell holds fewer than twenty files when the pass ends, a second pass over indices the first did not
touch adds rows rather than the rate being changed mid-stream, so no file is measured twice and the
subsample stays systematic.

## 7. Adversary dispositions

`ADVERSARY.md` raised thirty objections on 2026-09-10, on `claude-opus-5`, before anything was
measured. Every one carries exactly one disposition. The adversary inspected the platform itself
(MediaWiki `paraminfo`, `Special:MediaStatistics`, and the `image` table dump schema), and two of its
factual claims were re-verified by the run before being acted on: the dump exists at 18 452 452 774
bytes dated 2026-09-04, and its first row carries `{"data":{"Make":"Apple","Model":"iPad Air",...}}`.

**29 changed, 1 accepted as a stated limitation, 0 rejected.** The attack landed, and it landed on the
part of the method nobody had an opinion about: the frame and the population, not the parser.

### Section 1 — inputs that would make the conclusion false

| # | Disposition | The change |
|---|---|---|
| 1.1 | changed | A third reading, `no-imageinfo`, sits beside `not-a-jpeg`. A File: page with no file behind it is not evidence about JPEGs. |
| 1.2 | changed | The per-file API call also asks for the full version list. Rows are reported as single-version or overwritten, separately. |
| 1.3 | changed | `metadata_region_complete` becomes a hard precondition: any row where an IFD offset pointed past the window is re-fetched in full before it is scored. |
| 1.4 | changed | Same rule, extended: a row whose walk never reached the start-of-scan inside the window is re-fetched in full. |
| 1.5 | changed | Three bounds inside the parser — a visited-offset set, a ceiling on entries and computed lengths, and a per-file wall-clock timeout — each writing `parse_error` instead of propagating. |
| 1.6 | changed | `serial_value_class` with the values `empty`, `all_zero`, `all_same_character`, `plausible`. A class, never a value. |
| 1.7 | changed | The question's scope sentence now says JPEG files, and the make-bearing subset is reported beside the whole as the photograph proxy. |
| 1.8 | changed | The denominator rule is fixed: a row enters a share only when its status is 200 or 206 and its `parse_error` is null. The excluded count prints beside every share. |
| 1.9 | changed | `Accept-Encoding: identity` is sent explicitly, and `range_honoured` comes from the `Content-Range` header, not from a byte-count comparison. |
| 1.10 | changed | The validation download is capped at 64 MiB; when the cap fires the row records `validation_skipped_size` and stays in the subsample. |

### Section 2 — confounds

| # | Disposition | The change |
|---|---|---|
| 2.1 | changed | **The serial arm is measured with a second, independently written instrument** (`exiftool`) that reads the MakerNote, and a per-make recall figure is published beside every per-make share. No make is reported without its recall. This requires one install — see Gate B. |
| 2.2 | changed | `xmp_serial_present` is added as a second presence flag; every per-make share is reported twice, with and without XMP serials. |
| 2.3 | changed | The four-digit capture year is emitted and shares are reported within year bands. |
| 2.4 | changed | The per-file API call also records the uploader and the upload comment; comments matching a known tool signature are classified, the rest are `unclassified`, and shares are reported within pathway. |
| 2.5 | changed | The uploader is stored hashed; every cell reports its distinct-uploader count; a cell is described rather than compared below five distinct uploaders. |
| 2.6 | changed | The same API call reads whether the file page carries a coordinate through a template or a structured-data statement. The ratio of page-coordinate-without-EXIF-coordinate to both-present bounds how much of the missing share is removal. |
| 2.7 | changed | `gps_coord_tag_present` splits into `lat_and_lon_present`, `both_refs_present` and `gps_ifd_but_no_coord`. |
| 2.8 | changed | The 150-file comparison against `exiftool` is the instrument's error rate. Two runs of the same code stop being called validation and are kept only as the window check. |
| 2.9 | changed | The expectation is restated as a rate **within** each make, which is the only form that separates manufacturer policy from manufacturer popularity. |

### Section 3 — what the author was not thinking of

| # | Disposition | The change |
|---|---|---|
| 3.1 | changed | **The design changes shape.** The location, make and model arms come from the complete enumeration in `commonswiki-latest-image.sql.gz`, which carries `Make`, `Model`, `DateTimeOriginal` and coordinates for every file. The byte-level frame is kept for the serial arm alone and is stratified by make so its cells fill. See Gate B. |
| 3.2 | changed | Both criteria become interval statements: a make differs when its interval excludes the overall rate. A minimum cell of thirty files and five distinct uploaders is pre-declared, and the multiplicity of pairwise questions is stated. |
| 3.3 | changed | The make-level comparison across four to six makes is what the design supports. The model table reports counts and does not compare. |
| 3.4 | changed | All three denominators are published. The primary one is fixed now: accepted JPEGs whose status is 200 or 206, whose parse is clean, and which carry a `Make` — the only population in which a missing serial means something. |
| 3.5 | changed | The scope sentence now reads "JPEG files on Wikimedia Commons, as the platform serves their current version today". |
| 3.6 | changed | A third abandonment trigger: after 200 accepted files, stop if `metadata_region_complete` is false on more than a pre-declared share, or if agreement with `exiftool` on the first fifteen validation files falls below a pre-declared level. |
| 3.7 | changed | The acceptance rate is checked against the platform's own 80.9 per cent JPEG share and published as the first result, ahead of either share. The draw shortens its window by using the largest permitted page size. |
| 3.8 | changed | The write-up says in one sentence that the prior comes from the same instrument and is not independent corroboration, and that `exiftool` is what would break the agreement. |
| 3.9 | changed | Each row carries the raw string and a normalised key; grouping is on the normalised key, and the normalisation map is published so it can be argued with. |
| 3.10 | changed | `data/units.jsonl` stores a hash of the title, not the title. Titles stay in `data/frame.jsonl`, which is what reproducibility needs. Only aggregates are published. |
| 3.11 | accepted as a stated limitation | The between-model comparison is not made. The article states that the model table is a count and a per-model interval would need about a hundred files per cell, which a random draw does not produce. |

## 8. Amendments

Append-only. Nothing here was written after a result was seen: no file has been measured at the time
of writing.

- **2026-09-10T22:05Z, post_hoc: false.** The method was frozen at 21:50Z and the adversary returned
  at 21:56Z; the dispositions above were folded in afterwards, which is the order the adversary
  document prescribes ("written into the pre-registration as an amendment before the file is
  committed"). The freeze was therefore reset and re-taken, and the reason is recorded here rather
  than silently. Recorded as deviation D-01. Proposed to the skill: `freeze` hashes the whole file
  while section 8 is append-only by design, so the two rules contradict each other.
- **2026-09-10T22:05Z, post_hoc: false.** Section 2's acceptance rule, section 3's expectation and
  section 5's abandonment condition are superseded by the dispositions in section 7 where the two
  disagree. The original text stands as written, above, unedited.
