# EDH registration-slice ingest — Popper Engine V2, rung 2

**Date:** 2026-08-05. **Scope:** execute `docs/GOAL_POPPER_ENGINE_V2_PROGRAMME_20260804.md`
Sec.6 DECISION ENTRY (EDH adjudicated as the rung-2 substrate) by ingesting the
**registration slice** (first half of EDH's provinces) ONLY, holding out the second half
under the discipline stated in the launching brief. Access routes and licence per
`docs/generated/popper_v2_rung2_access_verification_20260804.md`. **No git commit taken**
(per instruction) — new files are untracked in the working tree; a concurrent, unrelated
session's edits to `apps/scriptome/models.py` / `build_extant_counts.py` (an "origin rule
attribution" layer, `docs/DESIGN_ORIGIN_HOLDINGS_LAYERS_20260805.md`) were observed live
mid-task, confirmed untouched by and non-conflicting with this work, and left alone.

**Result summary:** 33/33 registration-slice provinces harvested, 0 failures, 42,009
inscriptions ingested, per-province reconciliation against EDH's own live counts matches
**exactly** on all 33 rows (42,009 declared = 42,009 ingested), 65 raw API-response files
saved as possession-timestamp evidence (SHA-256 per file below), 0 holdout-province files
or queries anywhere in the run.

---

## 1. Province list and the alphabetical split

**Source:** EDH's own published API documentation, `https://edh.ub.uni-heidelberg.de/data/api`
("Controlled Vocabularies" > "Province" table) — metadata/vocabulary, not record content.
Fetched live 2026-08-05 (`curl`, declared research UA, re-verifying
`docs/generated/popper_v2_rung2_access_verification_20260804.md`'s own access-etiquette
notes: both `edh.ub.uni-heidelberg.de` and `edh-www.adw.uni-heidelberg.de` return HTTP 404
for `/robots.txt`, i.e. no file — default-allow, re-confirmed this session).

The table lists 66 provinces in EDH's own fixed order. That order is a **case-sensitive
ASCII sort of the 3-letter province code** (verified: `sorted(codes) == codes` is True for
case-sensitive comparison, False for a name-sort or a casefolded-code sort — e.g. `"BrL"`
sorts before `"Bri"` only under case-sensitive code ordering, and that is exactly the order
EDH publishes). This project's split rule ("first half of EDH's own alphabetical province
list") is applied to EDH's own published order directly, rather than re-alphabetizing by
full province name — a deliberate choice to avoid substituting my own sort for "EDH's own"
list. **Robustness check:** the split midpoint was independently recomputed sorting by full
province *name* (casefolded) instead of code; both orderings place the boundary in the
identical location (between "Germania superior" and "Hispania citerior"), so the 33/33 split
below does not depend on resolving that ambiguity.

66 provinces / 2 = an exact 33/33 split, no remainder to arbitrate.

### Registration slice (33 provinces — ingested this run)

| Code | Province | Code | Province |
|---|---|---|---|
| Ach | Achaia | BrL | Bruttium et Lucania (Regio III) |
| Aeg | Aegyptus | Bri | Britannia |
| Aem | Aemilia (Regio VIII) | Cap | Cappadocia |
| Afr | Africa Proconsularis | Cil | Cilicia |
| AlC | Alpes Cottiae | Cor | Corsica |
| AlG | Alpes Graiae | Cre | Creta |
| AlM | Alpes Maritimae | Cyp | Cyprus |
| AlP | Alpes Poeninae | Cyr | Cyrene |
| ApC | Apulia et Calabria (Regio II) | Dac | Dacia |
| Aqu | Aquitania | Dal | Dalmatia |
| Ara | Arabia | Epi | Epirus |
| Arm | Armenia | Etr | Etruria (Regio VII) |
| Asi | Asia | Gal | Galatia |
| Ass | Assyria | GeI | Germania inferior |
| Bae | Baetica | GeS | Germania superior |
| Bar | Barbaricum | | |
| Bel | Belgica | | |
| BiP | Bithynia et Pontus | | |

### Holdout (33 provinces — declared, NOT fetched, NOT cached, NOT read)

| Code | Province | Code | Province |
|---|---|---|---|
| HiC | Hispania citerior | PaI | Pannonia inferior |
| Inc | Provincia incerta | PaS | Pannonia superior |
| Iud | Iudaea | Pic | Picenum (Regio V) |
| LaC | Latium et Campania (Regio I) | Rae | Raetia |
| Lig | Liguria (Regio IX) | ReB | Regnum Bospori |
| Lug | Lugdunensis | Rom | Roma |
| Lus | Lusitania | Sam | Samnium (Regio IV) |
| LyP | Lycia et Pamphylia | Sar | Sardinia |
| MaC | Mauretania Caesariensis | Sic | Sicilia, Melita |
| MaT | Mauretania Tingitana | Syr | Syria |
| Mak | Macedonia | Thr | Thracia |
| Mes | Mesopotamia | Tra | Transpadana (Regio XI) |
| MoI | Moesia inferior | Tri | Tripolitania |
| MoS | Moesia superior | Umb | Umbria (Regio VI) |
| Nar | Narbonensis | Val | Valeria |
| Nor | Noricum | VeH | Venetia et Histria (Regio X) |
| Num | Numidia | | |

This exact pair of lists is now **registered** (hardcoded as
`EDH_REGISTRATION_SLICE_PROVINCES` / `EDH_HOLDOUT_PROVINCES` in
`apps/scriptome/services/edh_census.py:196-263`, not re-derived per run) so the partition
cannot drift if EDH's own page ever changes. `Inc` ("Provincia incerta" — an uncertain-
province catch-all, not a real geographic province) and `Rom` ("Roma") both land in the
holdout half by the mechanical rule and are left there — flagged, not special-cased, per the
task's "list, don't improvise" instruction.

---

## 2. Fetch routes used, and the monolithic-dump refusal

`https://edh.ub.uni-heidelberg.de/data/download` (fetched live 2026-08-05) offers exactly
two bulk-dump shapes, **both confirmed monolithic with respect to the province partition**
and therefore **not downloaded**:

- `edh_data_text.csv` — one file, all ~82,000 inscriptions, every province.
- `edhEpidocDump_HD000001-HD010000.zip` … `..._HD080001-HD082828.zip` (9 files) — chunked by
  sequential HD-number range, which is acquisition/publication order, not province; every
  chunk necessarily mixes registration-slice and holdout provinces.

Both routes were therefore ruled out per the launching instruction ("if the bulk dump is
monolithic... do NOT download it") without opening either — not even a byte-range peek at
`edh_data_text.csv`'s header row, since any read of that file risks pulling holdout-province
data rows along with it.

**Route actually used:** the live per-province query API,
`GET https://edh.ub.uni-heidelberg.de/data/api/inschrift/suche?provinz=<code>&limit=1000&offset=<n>`
— the only route capable of honoring the partition. Confirmed via `/data/api` documentation
(fetched live) to accept `provinz` (province code) as a first-class filter with a documented
controlled vocabulary (the same table Sec.1 uses), paginated JSON responses
(`{"total", "limit", "offset", "items"}`), no login, no key.

**Politeness:** `EDH_REQUEST_SLEEP_SECONDS = 1.8` (`apps/scriptome/services/edh_census.py:171`)
between every HTTP request including retries, per the launching instruction's explicit
"1-2s spacing" (EDH publishes no `robots.txt` and no documented rate limit, so there was no
stricter house number to defer to). User-Agent: the project's standing
`SCRIPTOME_USER_AGENT` ("InferpediaScriptome/0.1 (plerom.org/inferpedia; manuscript-
production research)"), matching the CDLI/korcis harvesters, not the separate verification-
lane UA. 33 provinces, 64 pages estimated from a totals-only probe, 65 pages actually
fetched (one extra: Armenia's page was fetched twice across a validation test-run and the
full run — same file, idempotently overwritten, same content/hash both times since EDH's
data has been frozen since Feb 2021).

**Raw-file storage:** `apps/scriptome/data/` was checked and found to hold only small
config/registry YAML (`catalogue_registry.yml`, `region_aliases.yml`, etc.) — no project
harvester stores raw payload dumps there. The actual convention (confirmed via
`SCRIPTOME_EXPORT_DIR` in `apps/scriptome/services/harvesting.py:34` and the existing
`mpo-drop`/`digital-scriptorium-drop`/`iran-union-fankha-drop`/etc. sibling directories under
it) is `/data/extracted_text/inferpedia-exports/scriptome/<slug>-drop/`, a Docker-volume
path outside the git tree. This harvest followed that convention:
**`/data/extracted_text/inferpedia-exports/scriptome/edh-drop/`** (inside the `inferopedia-
web-1` container), one file per (province, offset) page, written **before** any DB write
(`apps/scriptome/services/edh_census.py:_write_raw_page`), verbatim HTTP response bytes,
never touched again — the possession-timestamp evidence. `apps/scriptome/data/
catalogue_registry.yml` was deliberately **not** edited: this harvester follows the MPO/
CDLI/korcis "self-sufficient bootstrap" precedent (`get_or_create_edh_source()`, mirroring
`get_or_create_mpo_source` in `apps/scriptome/services/mpo_census.py:282-313`) and never
depends on the YAML registry having been synced — a disclosed scope choice, not an
oversight.

---

## 3. CatalogueSource and unit-modeling decisions

New `CatalogueSource` row, slug **`edh`**:

| Field | Value |
|---|---|
| title | Epigraphic Database Heidelberg (EDH) — registration slice (first-half provinces) |
| provider | Heidelberg Academy of Sciences and Humanities / Ruprecht-Karls-Universität Heidelberg |
| url | https://edh.ub.uni-heidelberg.de/data |
| license | CC BY-SA 4.0, quoted verbatim from `/data`, per the verification report |
| access_method | `api` |
| status | `active` |

### Inscriptions are not manuscripts — the target_unit/medium_class/tradition decisions

Considered and **rejected**: `apps/scriptome/models.py:769` `GraphosphericArtefact` (migration
0013). That table backs the Drupal-legacy museum-object classification pipeline — its whole
job is resolving *ambiguous* objects ("is this museum item text-bearing at all?" via
`graphospheric_class`/`classification_status`). EDH has no such ambiguity: every record is
definitionally an inscription. `ManuscriptRecord` — the table every other catalogue harvester
in this project mints into (CDLI, korcis, papyri.info, MPO, digital_scriptorium, ...) — is
the right home, provided the unit fields correctly firewall it from the manuscript census.

**`target_unit="physical_object"`** (`apps/scriptome/models.py:73`), deliberately **not**
`"physical_manuscript"` even though that is what the two closest precedents chose:
`cdli_census.py:274` (cuneiform tablets) and `papyri_census.py` both mint
`target_unit="physical_manuscript"` so those ancient objects *would* be swept into the
manuscript survival census. That choice does not transfer to EDH: `extant_counts.py`'s own
admission gate —

```
apps/scriptome/services/extant_counts.py:186-189
target_unit_allowed = (
    target_unit == "physical_manuscript"
    or (east_asia_census and target_unit in {"juan", "title", "physical_object"})
)
```

admits `physical_manuscript` **unconditionally**, regardless of tradition — minting EDH rows
that way would silently inflate the manuscript production/survival census the moment *any*
tradition's extant counts are rebuilt, directly violating this task's "never sum inscriptions
with manuscripts anywhere" instruction and goal-doc Sec.10's rung-2 census/estimates
firewall. `physical_object` is the existing vocabulary entry already excluded by that same
gate for every non-East-Asian call (the only door it has is `east_asia_census=True` for one
of the nine `EAST_ASIA_TRADITIONS`, `apps/scriptome/services/traditions.py:47-56` — see next
paragraph for why that door is also closed here).

**`tradition="other"`** (`apps/scriptome/models.py` `SCRIPTOME_PROFILES`), not `"latin_west"`
despite the corpus being overwhelmingly Latin/Roman-world. Verified live against the running
DB: 22 traditions have a registered `ScriptomeTradition` row; `"other"` is not one of them,
and it is not in `EAST_ASIA_TRADITIONS`. It can therefore never be swept into a tradition-
scoped census/cartogram/extant-count rebuild by construction — belt-and-braces alongside the
`target_unit` firewall, matching the goal doc's explicit framing of rung 2 as a domain
outside the Graphosphere manuscript-tradition census, not a Latin-West sub-bucket.

**`medium_class="unknown"`**, not `"manuscript"`. Papyri.info's own precedent
(`papyri_census.py` module docstring) deliberately chose `medium_class="manuscript"` for
papyri with the reasoning "a papyrus IS a handwritten single-object text-bearing item" — that
reasoning does not transfer to carved/incised/cast stone and bronze epigraphy, so this
harvest does not reuse it. The honest granular value (EDH's own `material` field, e.g.
"Marmor", "Kalkstein", "Bronze") goes into `ManuscriptRecord.material` verbatim, the same
resolution shape papyri.info used for its own medium_class gap.

**`repository=None`** — a disclosed gap, not a guess, following papyri.info's own precedent
verbatim ("this wave leaves `repository=None`, an honest named gap",
`apps/scriptome/services/papyri_census.py:49`). EDH's `present_location` field (e.g. "Roma,
Mus. Naz. Rom.") is free-text city+institution with no reliable delimiter to split on across
an unexamined 42,009-row corpus (unlike CDLI's semicolon-delimited `collections` field).
`present_location` is preserved verbatim in `raw` for every record; no `Repository` rows were
created by this harvest.

**`record_type="manuscript"`** (the model's own default) — kept as-is; the field
distinguishes a source's primary catalogue row from a `provenance_entry`/
`institutional_holding` sub-row, not the physical medium, and every EDH row is a primary row.

**`title`** ← `type_of_inscription` (EDH's own 16-value controlled vocabulary, e.g.
"epitaph", "mile-/leaguestone", "votive inscription") — the closest available analogue to
CDLI's own `designation`-as-title precedent (a short genre/type label standing in where no
literary title exists), left blank when EDH supplies no type.

**`origin_place`** ← `findspot_ancient`, falling back to `findspot_modern` then bare
`findspot` (mirrors the CDLI `provenience`/MPO `entstehungsort` precedent of using the
*origin*-flavoured field, never a current/reuse-context field — EDH's plain `findspot` field
sometimes literally describes secondary reuse, e.g. "..., sekundär verwendet").

**`extent_leaves`**: always `None` (not applicable to inscriptions). **`script`**: always
`""` (EDH's API carries no palaeographic-script field). All other EDH fields (`commentary`,
`transcription`, `diplomatic_text`, `literature`, `trismegistos_uri`, `type_of_monument`,
`work_status`, `responsible_individual`, `height`/`width`/`depth`/`letter_size`,
`last_update`, `year_of_find`, `country`, `modern_region`, `religion`, `present_location`,
`findspot`/`findspot_modern`) are preserved **verbatim, unabridged, in `raw`** — per the
standing preserve-source-fields rule, which names inscription/description fields as the
canonical example of what must never be stripped. Verified live: 0 `content_hash` collisions
across all 42,009 rows (no accidental exact-duplicate payloads).

### A genuine data-quality finding: `not_before`/`not_after` are swapped

Measured live against 1,000 Achaia records before committing to a parsing rule: of 378 rows
carrying **both** `not_before` and `not_after` with different values, `not_before > not_after`
in **all 378** (0 counter-examples) — e.g. `{"not_before": 200, "not_after": 101}` for a
record datable 101-200 AD. The API docs' own query-parameter names (`dat_jahr_a`="year not
before", `dat_jahr_e`="year not after") imply `not_before <= not_after`; the live JSON
response keys evidently run the opposite way from their own gloss. `resolve_edh_date_bounds()`
(`apps/scriptome/services/edh_census.py`) sidesteps the mislabeling entirely by taking
`min()`/`max()` of whichever values are present rather than trusting either key's name.
Verified across the full 42,009-row ingest: **0 records** have `date_start > date_end`.
33,984 records (80.9%) resolved a date at all; date_precision breaks down as range=32,453,
unknown=8,025, exact=1,531 (a single present value is treated as an exact point-year — a
disclosed mechanical simplification, not a verified claim about EDH's own internal
confidence). Observed range: -400 to 1998 — the 1998 upper bound is very likely an EDH data
quirk (a modern find-date or similar bleeding into a dating field) rather than a genuine
ancient attestation; preserved as-is from source, not silently corrected, and flagged here
rather than improvised over.

---

## 4. Preserve-source-fields compliance

Every EDH field observed in the API payload — including the inscription text fields
(`transcription`, `diplomatic_text`) and the free-text `commentary`/description field — is
copied verbatim into `ManuscriptRecord.raw` with no truncation, no stripping, no binary-field
removal, satisfying the standing operator rule. The only additions to `raw` are a
`_scriptome_harvest` provenance block (source slug/provider/access_method/license text,
`province_code`, `province_name`, `goal_source`) — the same pattern MPO/CDLI use — never a
subtraction.

---

## 5. Verification: per-province reconciliation

Each province's `declared_total` below is EDH's own live `total` field, read from the first
API page fetched for that `provinz=` query at harvest time — the identical number the human
search UI's result count reflects, since both are backed by the same live query (EDH's search
page itself is a client-rendered form with no server-embedded facet counts to scrape
separately, confirmed by reading its raw HTML; the API total *is* the search-facet count,
not a proxy for it). `ingested_count` is a fresh DB query (`ManuscriptRecord.objects.filter(
source=edh, raw__contains={"_scriptome_harvest": {"province_code": <code>}})`), independent
of the harvest run's own self-reported counters — verified functionally against real
inserted/deleted test rows before being trusted for this table.

| Code | Province | Declared total (API) | Ingested (DB) | Match | Pages |
|---|---|---|---|---|---|
| Ach | Achaia | 1283 | 1283 | YES | 2 |
| Aeg | Aegyptus | 167 | 167 | YES | 1 |
| Aem | Aemilia (Regio VIII) | 211 | 211 | YES | 1 |
| Afr | Africa Proconsularis | 4440 | 4440 | YES | 5 |
| AlC | Alpes Cottiae | 475 | 475 | YES | 1 |
| AlG | Alpes Graiae | 76 | 76 | YES | 1 |
| AlM | Alpes Maritimae | 413 | 413 | YES | 1 |
| AlP | Alpes Poeninae | 201 | 201 | YES | 1 |
| ApC | Apulia et Calabria (Regio II) | 761 | 761 | YES | 1 |
| Aqu | Aquitania | 426 | 426 | YES | 1 |
| Ara | Arabia | 234 | 234 | YES | 1 |
| Arm | Armenia | 3 | 3 | YES | 1 |
| Asi | Asia | 491 | 491 | YES | 1 |
| Ass | Assyria | 0 | 0 | YES | 1 |
| Bae | Baetica | 3016 | 3016 | YES | 4 |
| Bar | Barbaricum | 172 | 172 | YES | 1 |
| Bel | Belgica | 1711 | 1711 | YES | 2 |
| BiP | Bithynia et Pontus | 289 | 289 | YES | 1 |
| BrL | Bruttium et Lucania (Regio III) | 246 | 246 | YES | 1 |
| Bri | Britannia | 4471 | 4471 | YES | 5 |
| Cap | Cappadocia | 66 | 66 | YES | 1 |
| Cil | Cilicia | 70 | 70 | YES | 1 |
| Cor | Corsica | 46 | 46 | YES | 1 |
| Cre | Creta | 66 | 66 | YES | 1 |
| Cyp | Cyprus | 34 | 34 | YES | 1 |
| Cyr | Cyrene | 88 | 88 | YES | 1 |
| Dac | Dacia | 3582 | 3582 | YES | 4 |
| Dal | Dalmatia | 7647 | 7647 | YES | 8 |
| Epi | Epirus | 138 | 138 | YES | 1 |
| Etr | Etruria (Regio VII) | 651 | 651 | YES | 1 |
| Gal | Galatia | 208 | 208 | YES | 1 |
| GeI | Germania inferior | 3495 | 3495 | YES | 4 |
| GeS | Germania superior | 6832 | 6832 | YES | 7 |
| **Total** | **33 provinces** | **42,009** | **42,009** | **YES (33/33)** | **65** |

**Additional integrity checks (full 42,009-row scope, all passed):**
- Distinct `external_id` count = 42,009 (no cross-province duplicate HD-numbers; 0 flagged
  by the per-province duplicate-id check either).
- `target_unit` = `physical_object` on all 42,009 rows; `medium_class` = `unknown` on all
  42,009; `tradition` = `other` on all 42,009; `record_type` = `manuscript` on all 42,009.
- `repository` is `NULL` on all 42,009 (the disclosed gap, applied consistently).
- 0 `content_hash` collisions.
- 0 records with `date_start > date_end`.
- Idempotency verified live: re-running the harvest against an already-ingested province
  (Armenia, run twice — once as an isolated pipeline test, once inside the full 33-province
  run) produced `created=0, updated=3` the second time, no duplicates.

**Failed provinces: none.** `provinces_attempted=33, provinces_succeeded=33,
provinces_failed=0` — the command's own `failed` list is empty; nothing was skipped,
improvised around, or silently dropped.

---

## 6. Holdout-discipline proof

- `EdhHoldoutViolation` is raised by `_assert_registration_slice_province()`
  (`apps/scriptome/services/edh_census.py`) before **every** HTTP request this module makes;
  live-tested this session (`--provinces Rom` → `CommandError: Refused to fetch province
  'Rom': it is in the DECLARED HOLDOUT half...`, zero network calls made).
- The raw-file directory (`edh-drop/`, 65 files) contains **only** registration-slice-coded
  filenames — checked programmatically against `EDH_HOLDOUT_CODES`: zero matches.
- No bulk-dump file (`edh_data_text.csv`, any `edhEpidocDump_*.zip`) was fetched at all this
  session (verified: no such files anywhere under `edh-drop/` or elsewhere in the container's
  data volume from this run).
- The only holdout-related network activity this session was reading EDH's own **published
  province-vocabulary table** (Sec.1) — metadata, explicitly authorized by the task's Step 1
  ("Establish EDH's province list from its own published documentation/API (metadata, not
  record content)") — and reading the `/data`/`/data/download`/`/data/api` **documentation**
  pages, never a `provinz=<holdout code>` query of any kind, not even a count-only one.

---

## 7. File hashes (possession-timestamp evidence)

65 files, `/data/extracted_text/inferpedia-exports/scriptome/edh-drop/` inside the
`inferopedia-web-1` container, total 39,595,097 bytes. SHA-256 computed over the exact bytes
persisted to disk (the raw HTTP response body, before any parsing). Aggregate manifest
digest — `SHA-256` of the sorted `"<province> <offset padded to 6 digits> <file sha256>"`
lines, one convenience single-hash summary of the full set —
**`04f95968446bd2f2550c30f3acb5a1780fbe95797a6c001b48c823975722eb58`**.

| Province | Offset | Bytes | Items | SHA-256 |
|---|---|---|---|---|
| Ach | 0 | 1068713 | 1000 | `d03eb4687f5612bea75eec68659fe1c948a016b95f3a9ae93f35cfa97b561021`[^1] |
| Ach | 1000 | 279400 | 283 | `553978ec2433006fb1c8f107e4dff22dcd374c7153ece980ce876962647dcd49`[^1] |
| Aeg | 0 | 206821 | 167 | `bd70526d0a778c27b14d17e175c743c662df9346b3e758215c382295525488c1`[^1] |
| Aem | 0 | 233853 | 211 | `8e77b794f44fa0c783a53479db7e9233c18cae308089bc792ccf5daa00529fdc`[^1] |
| Afr | 0 | 724511 | 1000 | `0ed764a3e74f3825d491e3d1c53a1e370c5c7d247f2a9fb57caf6fca9aa267c3`[^1] |
| Afr | 1000 | 837316 | 1000 | `60ad3f4aa9b16e9dfbda0406bf3f0b5b5b8687c0dbd640168f085d9d95725445`[^1] |
| Afr | 2000 | 772678 | 1000 | `fc19cd75a4bd3c8939d8269126f24c2a73175408b2dd5655f6efb2dbaeb5ff23`[^1] |
| Afr | 3000 | 811985 | 1000 | `55b0f488bb0a4c68e4b2e22ac76f834b5c9447ca948deb1f7c6b619348c90612`[^1] |
| Afr | 4000 | 355727 | 440 | `fbbc27b44a072bca22fd37ae4c8f15ff729b6794d0c456dcd7ce2e1a63e93c60`[^1] |
| AlC | 0 | 469614 | 475 | `8c9f7f092280793ac42cb392a66845a0670e124875553f12c8296dcf67a9d842`[^1] |
| AlG | 0 | 90703 | 76 | `6d522fafeac59c969be4cad2bd123d0f640c47349d292e62c89cc943e7454a6d`[^1] |
| AlM | 0 | 444390 | 413 | `d2adeb4e0807108855271aa22b8609e86b30ac6a8ceaf4880a4a3f3bececcc7f`[^1] |
| AlP | 0 | 225648 | 201 | `ba21897f3eb1905bcf0a723aa5429e2e5bc4dc7309765bfa1f7528eaa31ab2d2`[^1] |
| ApC | 0 | 542080 | 761 | `4023905a6154ecb7e93ec2fbf38f0671ddd91d9bab270254b9bea5731c153fb7`[^1] |
| Aqu | 0 | 257893 | 426 | `a816887355ddd0483a1d18021e1a5cc849ceb3a2c1e1b079cbf2fbfd7e884647`[^1] |
| Ara | 0 | 179983 | 234 | `a9e013a186668da2696f6e64d010b30e09dc9dec0961d996f8ed270de7760268`[^1] |
| Arm | 0 | 3250 | 3 | `db6de050c041d1ed237300c37e5e8d6cd0cc54fcc7e1061fa90c7b7051483cbf`[^1] |
| Asi | 0 | 540105 | 491 | `a18513fbd07943015ec5d115d21f0501423f5b17b40e70729b9e92b297252f03`[^1] |
| Ass | 0 | 51 | 0 | `c26e36ecaef01b4fe5ff7ee68f621208c167af6a0865615997ce9e7e84e1eee7`[^1] |
| Bae | 0 | 915155 | 1000 | `db94ba604c9a45d03f44fd95838a63ef998ce3c342994a12fb01ad9f3cc9fcf4`[^1] |
| Bae | 1000 | 739480 | 1000 | `ad90b4f5def799ade9c6fc84db8a3aac030e1d70d735e59a56bcaf8bf671e49d`[^1] |
| Bae | 2000 | 855235 | 1000 | `928ee776ac411973c525992141aea09d4637a407dca438e18aa2b921cfa1e65f`[^1] |
| Bae | 3000 | 14384 | 16 | `1b9f982556caecaedfd30848e880b9135ba9952e2c9bee76b594b8cf593c57a5`[^1] |
| Bar | 0 | 151468 | 172 | `5b9f4d62459ca7f347252ac39eea814deb0e85f480e571beabce84371f2dc5de`[^1] |
| Bel | 0 | 786015 | 1000 | `48212498d80804dcd2691d31c8f9623eee0c3964f11e6880aeba1d6f37eb58d7`[^1] |
| Bel | 1000 | 644608 | 711 | `dad14798e4a087c5e37bb0cb7966fb96497db242c7bff5205abf1f1d5621135e`[^1] |
| BiP | 0 | 290215 | 289 | `de9ac246d78b5ea7ecc1ca11fdece7c2cbd43f33931b402f8b8f00b1a710824d`[^1] |
| BrL | 0 | 177315 | 246 | `a45c6277097a8178ac8e7435fff86568f8144de650caed438466947676738ed5`[^1] |
| Bri | 0 | 954420 | 1000 | `8ef5919cae42f036452fc0cabdd3dc1cd4d92ddcbdbd9a3cad00bcd109ebd391`[^1] |
| Bri | 1000 | 815741 | 1000 | `c947bd849aa88f8bc9847fd27e840d87536fa1d6cbc751c141c832e6a87f0d2e`[^1] |
| Bri | 2000 | 882440 | 1000 | `818d9b74ca4a4503113867684d518d58aa51200b1a02f32a9ff5e17f7d3aa3a5`[^1] |
| Bri | 3000 | 835251 | 1000 | `30f4c6eb635249ba0d99621629ef90a39642702983f44c00ceef51267b4b3629`[^1] |
| Bri | 4000 | 386420 | 471 | `05e87c56361cb06bac2adbb15e400483c41e07717b345e52e0e14bc77027169f`[^1] |
| Cap | 0 | 60652 | 66 | `40af94c9845523e88048ecf7c36855cebfae4148b74b52171efed50c3b30896a`[^1] |
| Cil | 0 | 86822 | 70 | `afc0b68ecba8be0cad60d09948a89eaf2732c8a19e029c17fc6c28c6b43ce85e`[^1] |
| Cor | 0 | 38276 | 46 | `f03ebdd8b8753f51a74c3183cc4ec6ad6ef05e31116657467eee0e2679191436`[^1] |
| Cre | 0 | 60095 | 66 | `5f5b9d07107fa97b616ac542e34a4f0d583a1f25668484f0297f8c68564303f4`[^1] |
| Cyp | 0 | 35265 | 34 | `c1359db996758d4bdc089f0d497d1c21f321b835b05d496c55d81dcef24e5518`[^1] |
| Cyr | 0 | 63992 | 88 | `d9ff06efbf0d4706b603b3f9ce02f9939f4203db24791f0629a27e4c62005583`[^1] |
| Dac | 0 | 1120891 | 1000 | `a6217dca246d936ec3e6c735189b310985434870d652c3f86611102ea0d66151`[^1] |
| Dac | 1000 | 972256 | 1000 | `b73d1f6d46046d73954bd6930ddabc6e5404031ab1384dd3ada882ef450d0b5d`[^1] |
| Dac | 2000 | 889936 | 1000 | `735e836c94fa6ec79976c95d5008a7b6d4c2be02296f474774805972edc30668`[^1] |
| Dac | 3000 | 512511 | 582 | `20ed8a5d1c97f18ac821f81cefdea2350cf69889c7b3862be46d8faf640886ad`[^1] |
| Dal | 0 | 1029841 | 1000 | `e50a74d06a1697680e8a1fd530f2bfde4191a1888fd3d2cec384cde2228d4c08`[^1] |
| Dal | 1000 | 1026500 | 1000 | `7c4ce94c7ec51e9820a450ee6924e8ed09d3abdc529d65e5c59d3b19daa60493`[^1] |
| Dal | 2000 | 997313 | 1000 | `19d51fe78637719323f01d3a27c8503b4a8a3be2e6e137607e656a9a1281d7d8`[^1] |
| Dal | 3000 | 927517 | 1000 | `f080aab7a8f125209d34db39819a35b73dac056c233ab290cb32513297822190`[^1] |
| Dal | 4000 | 819098 | 1000 | `afe92e1aa28fbc6e399d102ac6189f6bce8ff1c5880293c51936f73a7745860c`[^1] |
| Dal | 5000 | 800538 | 1000 | `1b8080080afca60ef0c98f7d2501e7500e8b58b67b4c14866923cf8cade207b6`[^1] |
| Dal | 6000 | 767668 | 1000 | `fd503052cdb8e64a979a3c1558644e589d8de2642d6adec48b28fdd30ef6eb69`[^1] |
| Dal | 7000 | 622199 | 647 | `a1ad6d9b6941ce7efb24a6469366be57162e329a24aa2ac3798e241695655c24`[^1] |
| Epi | 0 | 151886 | 138 | `56bb74dc26d4add5523b1a2a84fad72556a109ab9cc651970d162505729ef5b9`[^1] |
| Etr | 0 | 605519 | 651 | `311532c118604377e12d17b082e7597d0966532da6a37058737657e524a1827c`[^1] |
| Gal | 0 | 184611 | 208 | `70337fda361c1a0e691f895a265e40e00b4cffa89da60a175f5a5b328b4acb99`[^1] |
| GeI | 0 | 1132040 | 1000 | `35767653c8fc2a2a9ef33ee27e2bd3fffd4ae957d611a96e4a24d4d9a51ed949`[^1] |
| GeI | 1000 | 1050086 | 1000 | `af1bcf1edadb0881dc59ae1c08f9a672f8ff9c6606a1080f34c6140abe65117d`[^1] |
| GeI | 2000 | 971699 | 1000 | `8e469bcdd415b989181ee58271c87a6552fca3a46521f4aaf9f3656cc86c04db`[^1] |
| GeI | 3000 | 412475 | 495 | `6be2906dce2139417438755bf5e54efa3f5d29f1de189c4c7dd92e72c77be6f3`[^1] |
| GeS | 0 | 1308104 | 1000 | `b45d275bab40f7fcd3e56fd7ec49181cc27b53be24a11ed038e0544ce15ae5a8`[^1] |
| GeS | 1000 | 1276021 | 1000 | `bc54e6350214a2dab1621c676cbf6f65f7c3610c255772245188d5a86138440a`[^1] |
| GeS | 2000 | 1194882 | 1000 | `84ff7500dedeb5ff43f7d15dadb4e53bb799b6c662f04daf819b487789ab17d9`[^1] |
| GeS | 3000 | 1086106 | 1000 | `0f7acc461431d3b69493a70a25a5f226b5b77e14faa9672db5a55cc366bb7361`[^1] |
| GeS | 4000 | 963127 | 1000 | `1d0fbb11131565af1a482387d3ae1e32569be4bbb6dd77269f654d4ca2dcfe7c`[^1] |
| GeS | 5000 | 1014718 | 1000 | `844419b3e3d890ea89b3f692b8124ecf9707678c5363b444a081fdc5b8e8129d`[^1] |
| GeS | 6000 | 919585 | 832 | `96dc2c478f5e8002f31c84529a879b56cdda7985fcb0d3c6d9ab286f1499265f`[^1] |

[^1]: All 65 hashes are 64-character lowercase hex SHA-256 digests of the exact raw HTTP
    response bytes saved to `edh_<province>_offset<NNNNNN>.json`; verified programmatically
    (`len(sha256_hex) == 64` for all 65 files) before inclusion here.

---

## 8. Files touched this session

- `apps/scriptome/services/edh_census.py` — new. Harvest/ingest service module (holdout
  guard, fetch/retry/raw-persist, field mapping, per-province ingest, reconciliation query).
- `apps/scriptome/management/commands/harvest_scriptome_edh.py` — new. CLI wrapper
  (`--mode harvest|report`, `--provinces`, `--dry-run`).
- `apps/scriptome/data/catalogue_registry.yml` — **not** edited (deliberate, see Sec.2).
- No migration needed (no new model, no new choices-list value — `physical_object`,
  `unknown`, and `other` all pre-existed).
- Both new files were `docker cp`'d into the running `inferopedia-web-1` container (this
  project's live code is baked into the image at build time, not bind-mounted — confirmed
  via the Dockerfile and matches the standing "harvests run OLD in-container code until
  cp'd" lesson) and MD5-verified identical to the host copies before running anything.
- No git commit taken, per instruction.

## 9. What was NOT done (scope discipline, not oversight)

- Holdout provinces (33 of them, Sec.1) were never fetched, counted, or read in any form.
- The bulk CSV/XML dumps were never downloaded (Sec.2).
- `Repository` rows were not created (disclosed gap, Sec.3).
- `apps/scriptome/data/catalogue_registry.yml` was not synced (disclosed, Sec.2).
- Nothing was written back into any manuscript-production census, cartogram, or public count
  surface — this ingest only creates rows on the new `edh` `CatalogueSource`, firewalled by
  `target_unit`/`tradition` as detailed in Sec.3.
