de:wasserversorger-enrich
Enrich German water suppliers (Wasserversorger) with contact details and the published water-analysis URL, and append results to the campaign JSONL.
Manifest
de/tasks.yaml- Address
de:wasserversorger-enrich— alsode:wasserversorger-enrich- Runner
- claude-code
- Policy
- autonomous writes only inside its own workspace, no outward action
- Workspace
- countries/de/
- Schedule
30 2,7,12,17,22 * * *· next 08/09/2026 12:30- Model
- sonnet
- Budget
- 400 turns · 90 minutes
- Triggerable
- yes — free text can reach it
- Notifies
- every run — 🟢 on success, 🔴 on failure
- Description (manifest)
- Enrich German water suppliers (Wasserversorger) with contact details and the published water-analysis URL, and append results to the campaign JSONL.
Cost per run
oldest left · ceiling $100| When | What it reported | Turns | Took | Cost | |
|---|---|---|---|---|---|
| 08/09/2026 07:51 | Journal updated. The run is complete: batch of 45 water suppliers researched and merged, coverage stable, evidence trail intact, no forbidden actions taken. **Outcome:**… | 32 | 21m | $37.58 | ok |
| 08/09/2026 02:50 | Journal entry appended. Final summary for the report. Enriched 45 more German water suppliers (wasserversorger campaign): 39/45 got a full contact set (email/phone/addre… | 32 | 20m | $14.66 | ok |
| 07/09/2026 22:56 | Run complete. Enriched 45 water suppliers (23+22 shards, both validated and merged cleanly): 45/45 got address/phone/website, 41/45 email, 15/45 multi-zone, 237 fetches +… | 35 | 26m | $19.20 | ok |
| 07/09/2026 18:10 | Journal updated. Final report: Enriched 45 German water suppliers (batch tag `20260907-1530`, 2 parallel subagents per shard cap) — phone/address/website 45/45, email 43… | 33 | 40m | $34.70 | ok |
| 07/09/2026 16:05 | Both files clean. Run complete. **Outcome:** Enriched 45 more Wasserversorger (23+22 across two parallel research passes), bringing `enrichment.jsonl` to 2520 records (2… | 41 | 31m | $28.33 | ok |
| 07/09/2026 12:54 | Journal entry appended. The run is complete. **Outcome:** Enriched 45 more German water suppliers (Wasserversorger) via 2 parallel subagents (23 + 22, population-priorit… | 27 | 24m | $14.79 | ok |
| 07/09/2026 08:02 | This wasserversorger-enrich run completed cleanly: enriched 45 more water suppliers (2379→2424 of 3375 total, 976 pending), population coverage holds at 99.99%, and merge… | 19 | 2m | $46.20 | ok |
| 07/09/2026 03:07 | These are historical batch files from past runs (consistent with the campaign's convention of keeping them); today's own batch files were already removed. Everything is m… | 51 | 37m | $52.12 | ok |
| 06/09/2026 22:53 | Run complete. Enriched 45 more water suppliers (2289 → 2334 distinct, `enrichment.jsonl` 2294 → 2339 lines), all verified as genuine active suppliers with clean shard mer… | 37 | 23m | $17.77 | ok |
| 06/09/2026 18:10 | Journal updated. The run completed cleanly end-to-end. Processed a 45-company batch (23+22 shards) for the wasserversorger-enrich campaign: all verified as genuine drink… | 29 | 40m | $31.60 | ok |
| 06/09/2026 13:19 | Journal entry appended. The run is complete — batch of 45 water suppliers enriched, merged, and verified. Enriched 45 more German water suppliers (23+22 shards, both for… | 27 | 49m | $52.85 | ok |
| 06/09/2026 08:06 | Everything checks out. Final report: Enriched 45 more German water suppliers (23+22 split across two synchronous subagents), verified 45/45 distinct, zero collisions, an… | 36 | 36m | $31.93 | ok |
| 06/09/2026 03:03 | Everything is clean — no leftover temp directories, all files updated and consistent. The run is complete. Batch of 45 German water suppliers enriched (contact details +… | 61 | 33m | $47.26 | ok |
| 05/09/2026 23:10 | Everything checks out and the journal is up to date. Enriched 45 more German water suppliers (2064 → 2109 of 3375 on the roster; 1291 remain pending, none blocked on a m… | 47 | 40m | $36.72 | ok |
| 05/09/2026 18:29 | Consistent with convention — batch input files are kept, not deleted. Nothing further to clean up. This run is complete. ## Summary Ran the wasserversorger-enrich sched… | 32 | 59m | $30.17 | ok |
| 05/09/2026 13:01 | Everything checks out consistently. Not a git repo, so no commit needed — the data files are the persistence layer. ## Report Enriched 45 German water suppliers (23 + 2… | 39 | 31m | $26.81 | ok |
| 05/09/2026 07:56 | Journal entry appended. Run complete. **Outcome:** Enriched 45 German water suppliers (2 parallel research passes, 23 + 22 companies) — all verified as genuine drinking-… | 36 | 26m | $21.58 | ok |
| 05/09/2026 03:05 | Good, both entries are distinct and correctly placed. The run is complete. **Summary:** Ran the wasserversorger-enrich batch of 45 water suppliers (0 pending_domain in q… | 37 | 35m | $31.26 | ok |
| 04/09/2026 22:53 | Everything is consistent and merged. Run complete. **Outcome:** Enriched 45 German water suppliers (23+22 via two parallel foreground subagents) covering contact fields,… | 42 | 23m | $19.93 | ok |
| 04/09/2026 17:57 | Journal entry appended and the pipeline finished cleanly. Final report below. The scheduled 45-company batch (shard A: 23, shard B: 22) completed successfully — all comp… | 42 | 27m | $24.23 | ok |
| 04/09/2026 13:29 | Everything checks out. The run is complete. **Summary:** Enriched a 45-company batch of German water suppliers (population-priority order, 0 pending_domain in queue). Tw… | 51 | 59m | $97.62 | ok |
| 04/09/2026 07:57 | Journal entry appended. Run complete. **Outcome:** Enriched 45 German water suppliers (shard A: 23, shard B: 22), both researched directly by foreground subagents with n… | 35 | 27m | $29.95 | ok |
| 04/09/2026 02:52 | Journal entry written. The run is complete: all 45 companies enriched, verified, merged, and the campaign pipeline re-run clean. Enriched 45 German water suppliers this… | 44 | 22m | $54.39 | ok |
| 03/09/2026 23:06 | All state files are consistent and the campaign artifacts are up to date. Enriched all 45 batched Wasserversorger (population-priority order, 0 pending_domain), running… | 55 | 36m | $42.71 | ok |
| 03/09/2026 18:05 | Journal updated. Run complete. Enriched a 45-company batch of German water suppliers (`enrichment.jsonl` 1574→1619, 1614 distinct), all researched via 2 synchronous suba… | 30 | 35m | $39.88 | ok |
| 03/09/2026 16:25 | All done. Enrichment is complete and merged, the finish pipeline ran clean, and the journal is updated. **Report:** Enriched 45 German water suppliers (23+22 shards) — f… | 38 | 27m | $22.73 | ok |
| 03/09/2026 12:59 | Journal entry appended. The run is complete. Enriched 45 water suppliers this pass (23+22 shards), all research-only with no contact made and no captchas touched: 38/45… | 27 | 29m | $29.24 | ok |
| 03/09/2026 08:00 | Enriched 45 German water suppliers (wv-de-3792…wv-de-3816 range plus VG/Verband entries) — enrichment.jsonl grew 1439→1484, 272 new municipality links, 43/45 got an analy… | 32 | 30m | $26.20 | ok |
| 03/09/2026 02:53 | Run complete. Final report: Enriched 45 more German water suppliers (23+22 shards, both verified clean against the roster with zero overlaps/malformed records), adding 4… | 28 | 23m | $23.40 | ok |
| 02/09/2026 23:15 | Run complete: enriched 45 more German water suppliers (23+22 shards, 0 contact made, all Impressum-verified), merged cleanly into `enrichment.jsonl` (1349→1394 lines, 138… | 25 | 45m | $40.09 | ok |
| 02/09/2026 18:55 | Journal updated. Final report: Enriched 45 more German water suppliers (wasserversorger campaign): all research-only, no contact attempted, no captchas touched. Combined… | 30 | 40m | $34.66 | ok |
| 02/09/2026 18:08 | All shard evidence files are preserved as the campaign's convention requires, and the merge is clean. **Run complete.** Enriched 45 German water suppliers (research-only… | 31 | 38m | $46.79 | ok |
| 02/09/2026 17:12 | Journal entry appended. This run enriched 45 more German water suppliers (23+22 shards, synchronous subagents, no contact made, no captchas touched): `enrichment.jsonl` n… | 44 | 29m | $22.31 | ok |
| 02/09/2026 13:09 | Enriched 45 more German water suppliers (19 previously domain-blocked + 26 largest by population served): enrichment.jsonl grew from 1169→1214 records (1209 distinct comp… | 55 | 39m | $41.51 | ok |
| 02/09/2026 08:02 | All files validated. Final report: Processed a 45-company batch of German water suppliers (Wasserversorger). Recognizing the batch came from the same WWA Traunstein/Ansb… | 63 | 32m | $14.62 | ok |
| 02/09/2026 03:01 | Run complete. Enriched 45 German water-supplier companies (all `pending_domain` rows), appending to `enrichment.jsonl` (1060→1105 distinct companies) and 119 new municipa… | 51 | 31m | $18.13 | ok |
| 01/09/2026 23:19 | Journal entry appended. Final report below. Enriched 45 more German water suppliers (23+22 split across two synchronous subagents), including all 9 remaining `pending_do… | 31 | 49m | $49.94 | ok |
| 01/09/2026 18:00 | Enrichment run complete: 45 water-supplier companies researched (23+22 split across two synchronous subagents), all appended cleanly with no overlaps. `enrichment.jsonl`… | 25 | 30m | $26.80 | ok |
| 01/09/2026 13:09 | Journal updated. Final report below. Ran the wasserversorger-enrich skill: researched a 45-company batch (5 pending_domain + 40 by population served, standing duplicate… | 36 | 39m | $21.10 | ok |
| 01/09/2026 08:02 | Enriched 45 German water suppliers (23+22 split across two synchronous subagents), covering the 3 remaining `pending_domain` rows plus 42 by population served (34.4k–31.5… | 32 | 32m | $28.94 | ok |
| 01/09/2026 03:16 | Enrichment run complete: processed a 45-company batch (3 pending_domain + 42 by population served, 39.8k–34.6k residents), all appended and validated (0 malformed, 0 miss… | 29 | 46m | $42.05 | ok |
| 31/08/2026 22:48 | Run complete. Enriched 45 more water suppliers (13 `pending_domain` priority rows + 32 by population served), bringing distinct enriched companies to 835/3035. All 13 pen… | 38 | 18m | $11.17 | ok |
| 31/08/2026 17:57 | Journal updated. Run complete. **Outcome:** Enriched 45 companies (2 shards, 23+22, dispatched as synchronous subagents per the skill's memory-cap rule) — 18 of 19 `pend… | 36 | 27m | $20.67 | ok |
| 31/08/2026 13:27 | Everything checks out cleanly. Enriched a 45-company batch of German water suppliers (3 pending_domain rows prioritized, rest by population served, 6 known duplicates pr… | 61 | 57m | $31.56 | ok |
| 31/08/2026 08:03 | Run complete. Enriched 45 more German water suppliers (23+22 via two parallel research agents), appending contact/analysis records to `enrichment.jsonl` (655→700 distinct… | 40 | 33m | $40.14 | ok |
| 31/08/2026 03:26 | Everything checks out. All work completed synchronously within this run. **Outcome:** Enriched a 45-company batch (23+22 shards, split by pending-domain priority then po… | 51 | 56m | $50.72 | ok |
| 30/08/2026 22:58 | Temp script cleaned up correctly. **Run complete.** Enriched 45 water suppliers (2 parallel research shards, 23+22, both 100% complete) — 610 companies now have full con… | 35 | 28m | $21.59 | ok |
| 30/08/2026 17:58 | Journal updated. Final report: Ran the `wasserversorger-enrich` skill's 45-company batch (shards ee/ff, 23+22 companies split across two synchronous subagents), excludin… | 44 | 28m | $19.06 | ok |
| 30/08/2026 13:07 | Everything checks out. Journal entry appended. **Summary:** Ran the wasserversorger-enrich skill for a 45-company batch (excluding the standing duplicate-pair backlog),… | 43 | 37m | $17.01 | ok |
| 30/08/2026 08:10 | Enrichment run complete: 45 new companies enriched (430 → 475 of 2537, ~18.7%), population coverage nudged to 94.31% (+0.01pp), with the `pending_domain` priority row res… | 53 | 38m | $22.58 | ok |
| 30/08/2026 02:59 | Run complete: enriched 45 German water suppliers (contact fields + published water-analysis locator + municipality links), catching and excluding one known duplicate (`wv… | 41 | 29m | $28.36 | ok |
| 29/08/2026 23:10 | Run complete. Enriched 30 water suppliers (15 village-scale pending-domain candidates + 15 large city utilities like Stadtwerke Münster, badenova, ESWE Wiesbaden, ENTEGA)… | 45 | 40m | $24.42 | ok |
| 29/08/2026 18:05 | Journal entry appended. Run complete. **Summary:** Enriched 30 more German water suppliers (13 `pending_domain` village-scale rows + 17 by population, including 15 large… | 34 | 35m | $34.31 | ok |
| 29/08/2026 13:02 | Journal entry written. Final report: Enriched 30 more Wasserversorger companies (2 subagent shards of 15, both fully accounted for and validated before merge). The stand… | 45 | 32m | $22.89 | ok |
| 29/08/2026 07:51 | Run complete: enriched 30 more pending_domain water suppliers (6 got a verified official domain, 13 published water-analysis URLs found), pushing population coverage from… | 36 | 21m | $15.19 | ok |
| 29/08/2026 02:45 | Run complete. Enriched a 30-company `pending_domain` batch (2 synchronous subagents, 15/15 split): 5 official domains resolved, 25 stayed `pending_domain` with earned rea… | 38 | 15m | $8.90 | ok |
| 28/08/2026 22:57 | Those are kept as the evidence trail, consistent with prior runs' convention. Everything is merged and the journal is updated. Ran the wasserversorger-enrich batch: 30 p… | 44 | 27m | $20.97 | ok |
| 28/08/2026 17:56 | Journal updated. Run complete. **Outcome:** Enriched 30 pending_domain water suppliers (16+14 across two synchronous subagents), resolving 11 official domains and correc… | 46 | 26m | $19.09 | ok |
| 28/08/2026 12:58 | Everything checks out cleanly. Final report: Enriched 30 `pending_domain` water suppliers (2×15 shards) in this run, resolving 12 new official domains and adding contact… | 44 | 28m | $22.12 | ok |
| 28/08/2026 08:02 | Run complete. Enriched 30 water suppliers (all `pending_domain`-tier, population-priority order), resolved 7 official domains with zero collisions, and added 37 new munic… | 62 | 32m | $31.62 | ok |
The skill it runs
countries/de/skills/wasserversorger-enrich/SKILL.md · 245 linesRead the skill
Wasserversorger enrichment
You are in the country workspace. Campaign files: campaigns/wasserversorger/.
Plan and measured pilot results: _tasks/netzbetreiber-contact/german-water/PLAN.md.
This is the water counterpart of netzbetreiber-enrich. Same guards, same append-only
discipline, two differences that matter: the data is mostly published (so outreach is a
fallback, not the goal), and company identity is ours, not the register's.
Scope — this skill is Step 2, not Step 0
It enriches suppliers that are already identified. Finding out who exists is the separate
discovery sweep: coverage.mjs ranks the unclaimed municipalities by population into
residual-top.csv, a sweep resolves them into roster.part-*.jsonl + roster-links.part-*.jsonl,
and only then do those companies become enrichment input. Do not start here on an empty roster.
Batch selection
- Read the roster (
roster.part-*.jsonl, andseed.csvonce it exists) pluscampaigns/wasserversorger/enrichment.jsonl. A row is pending when itscompany_idhas no JSONL record. - Take the next 45 pending rows, unless requester instructions say otherwise (a different
count, or specific
company_ids to re-research). Water enrichment measured a median of 5 fetches per company against 1-2 for netzbetreiber, so 30 is the working batch — raise it only on measured evidence from the ledger, never on a guess. - Order the batch by priority, not by file order:
a.
pending_domainrows first. A company with noofficial_domaincannot be deduped and cannot be imported at all — domain is the dedup key. 107 of the roster are in this state as of 2026-08-27 and discovery keeps producing more, because it finds delegated suppliers by name off a Verbandsgemeinde page without resolving their own site. Clearing these unblocks the load and surfaces domain collisions while they are still few. b. Then by population served (sum thebevoelkerungof the municipalities linked to thatcompany_idinroster-links.jsonl— grep it, it is 3 MB+, never read it whole). The consumer is energiemarie's city→supplier finder, queried in proportion to population. c. Then the rest. - Append-only. A re-research appends a new record; exports take the latest per
company_id.
Identity — read before recording anything
company_id (wv-de-NNN) is our internal primary key. official_domain is the dedup/merge
key (decided by Stefanie, 2026-08-12). Do not key records on handelsregister_nr,
water_delegate_id, or any cities-api id. Mapping our companies onto cities-api rows is a
separate late-binding pass run at import time.
Why not Handelsregister (measured 2026-08-12, pilot 3): only 16% of suppliers have a number at all — Zweckverbände are Körperschaften des öffentlichen Rechts and are not in the register.
That 16% is a Zweckverband rate, not a national one. The population-weighted roster sweep (2026-08-12, 19 big-city suppliers) found 68% with a number printed in the Impressum: the head of the country is GmbHs and AGs, which are registered. Handelsregister coverage therefore correlates with size, which is exactly what makes it useless as a key — it is present where dedup is easy and absent where dedup is hard. Domain does not have that skew.
Legal forms that justify a null handelsregister_nr are KdöR and AöR — Berliner
Wasserbetriebe is an Anstalt des öffentlichen Rechts, not a Körperschaft. Record whichever the
Impressum states.
Worse, where a number exists it may belong to an operating subsidiary rather than the
public-law body holding the supply relationship (Aggerverband has no entry; AV Aggerwasser GmbH
is HRB 39328). Matching on it attaches the wrong company, undetectably.
Domain has full coverage instead (website_url was 100% in pilot 3) and the field already
exists in Stefanie's analysis contract as official_domain.
Normalise before comparing: lowercase, strip scheme and www., reduce to the registrable
domain (eTLD+1). https://WWW.Beispiel.de/wasser/ → beispiel.de.
Two failure modes to guard, not ignore:
- One domain, several suppliers. Small Eigenbetriebe live on the municipality's domain,
and German Samtgemeinden / Verwaltungsgemeinschaften often share one website across several
municipalities — so two genuinely distinct suppliers can present the same domain. Parent /
operating-subsidiary pairs do the same. Never auto-merge on a domain collision. Flag any
official_domainclaimed by more than onecompany_idinto a review queue and record both rows; a wrong merge here is the same undetectable error we rejected Handelsregister for. - One supplier, several domains. Rebrands,
.de/.compairs, and old domains that redirect. Record the domain you verified via the Impressum and put observed alternates innotes, so a later rebrand appends an alias rather than creating a duplicate company.
Priority input: pending_domain rows
Discovery records a delegated supplier it could name but could not locate — official_domain: null
with domain_status: "pending_domain" — because the VG page that names the supplier usually does
not link its website. Those rows block the load: domain is the dedup key, so a row without one
cannot be deduped or imported.
Take them before the normal batch. The company name is already verbatim in the row's notes
along with the page that named it; the job is to find the official site, verify the Impressum names
that entity, and set official_domain + domain_status: "resolved". If it genuinely has no
website — small Zweckverbände sometimes publish only through their member municipalities — say so
in reason and leave the status as is rather than attaching a member's domain, which would key the
supplier to the wrong organisation.
Research each supplier
Spawn at most 2 parallel subagents (each is a separate multi-GB claude process; the container has a 6 GB memory cap — 4+ concurrent agents OOM the run). Work supplier by supplier.
Part 1 — contact and detail fields
Target fields: name, short_name, address, phone, address_opening_hours,
phone_opening_hours, email, contact_form_url, website_url,
emergency_phone_number, supply_area_municipalities.
- Official website. Verify the Impressum names this supplier. For a Zweckverband the
Impressum usually also states the legal form (
Körperschaft des öffentlichen Rechts) — record that verbatim when present, it is what justifies a nullhandelsregister_nr. - Contact path. Kontakt / Kontaktformular. Record
contact_form_urlonly if an actual form is present, not merely an address block. - Emergency number. Look for
Notdienst,Störung,Entstörung,Havarie,Bereitschaft— often a separate page, not the Impressum. - Supply area. The member/
Versorgungsgebietlist. This feeds the pivot; see Part 3.
supply_area_plz is not collected. Nobody publishes it (0/25 in pilot 3) and it is
derivable: take the member municipalities and read plz from
_tasks/netzbetreiber-contact/german-water/gemeinden_deutschland.csv. Compute it downstream;
never scrape it, never email anyone for it.
Part 2 — the analysis locator
Port Stefanie's validated prompt contract verbatim — do not redesign it. Field list and her
worked example (Stadtwerke Kempen) are in
_tasks/netzbetreiber-contact/german-water/basecamp-todo-10101567321-find-prompt-analysis-url.md:
official_domain, analysis_url, index_url, url_type, tier, year, multi_zone,
zone_urls, zone_url_pattern, access_method, evidence_value, url_looks_dated,
third_party_fallback, confidence, notes.
Published analyses are obligatory under TrinkwV §45/§46, so expect to find one — pilot 3 got
21/25, which is 21 of the 21 entities that actually supply drinking water; the 4 misses were
all roster defects (flood-protection Verbände). multi_zone ran at 15/21 = 71% of real
suppliers (60% of all 25 rows), so treat multiple supply zones as the normal case.
Useful side effect: "no analysis page anywhere + no Trinkwasser content" is a cheap automated filter for a non-supplier that has crept onto the roster.
Part 3 — municipality links
One record per published municipality↔company link in links.jsonl, carrying the verbatim
published name. Normalisation and AGS resolution happen only in map_ags.mjs — never in
the crawl. The crawl records what the supplier published; every normalisation decision stays in
one reviewable file so the match rate is reproducible from raw evidence.
Classify each member with member_kind: gemeinde, ortsteil, landkreis, utility,
unknown. Landkreise and utilities are legitimate members but have no AGS — they must not
count against the match rate.
Set quartier when the supplier names a sub-area (Ortsteil / Versorgungszone) rather than the
whole municipality, and type to aep for drinking water.
Hard rules
Country rules in ../../CLAUDE.md apply in full (never invent contact data; research only,
never contact anyone; record a source per field group). Plus three the pilots proved:
- Every field carries its own
source_urlandconfidence. Not one per record. - A negative must be earned. Before recording
not_published, follow the site's own labelled links. Pilot 3's single false negative was anemergency_phone_numbermarkednot_publishedwhere the homepage linked "Notdienst" three times and the page was never opened. Usestatus:found/not_published(verified absent) /not_found(searched, inconclusive) /not_applicable(cannot exist for this legal form). - Label inference as inference. A
handelsregister_nrofnot_applicablejustified by an Impressum statement is a scraped fact; the same conclusion drawn from the legal form in the supplier's name is an inference. Say which inreason. 14 of 21 in pilot 3 were the latter — legally sound, but not evidence.
Memory safety
When searching saved HTML use fixed-string searches (grep -F -c 'g-recaptcha') or read the
file directly — never wide-context regexes like .{0,150}pattern.{0,150} or unbounded
.*pattern.* with -o. On minified single-line HTML these allocate gigabytes and have
OOM-killed the host (2026-07-22).
Fetch politely: no parallel hammering of a single host. Known host traps, all measured:
bodensee-wasserversorgung.deserves an incomplete TLS chain — curl and WebFetch both fail cert verification; it needs a CA workaround or it reads as unreachable rather than absent.78.46.40.200has hard-banned this IP after ~7 requests, and the ban is durable (still refusing an hour later, not a 429). Two campaign companies share that one host — reckenberg-gruppe.de and fernwasser-franken.de — so one ban took out two suppliers.l.de(Leipziger Gruppe) returns 403 to both curl and WebFetch. Bot protection, not absence. Record it as unresolved; do not conclude the data is unpublished.- Impressum paths are not guessable.
/impressum404s on Stadtwerke Düsseldorf (/rechtliches/impressum/), Stadtwerke Karlsruhe (/de/impressum/) and N-ERGIE (a long portal URL). Fetch the homepage and read the footer link rather than guessing paths. - Normalise Unicode before matching. One site's heading
Öffungszeiten(their typo) did not match agrep -Ffor the same string because theÖwas not the precomposed codepoint. - PDFs may be pure scans with no text layer —
pdftotextreturned 6 bytes for a supplier whose 26 zone reports are all Konica-Minolta scans. Render and read, or OCR; a pdftotext-only pipeline scores that entire supplier as "publishes nothing".
Record
Append one JSON object per supplier (one line) to enrichment.jsonl. Each field is an object:
{"company_id": "wv-de-001", "name": "<from seed>",
"fields": {
"email": {"value": "info@…", "status": "found",
"source_url": "https://…/impressum/", "confidence": "high", "reason": null},
"handelsregister_nr": {"value": null, "status": "not_applicable", "source_url": "https://…",
"confidence": "high", "reason": "Impressum states Körperschaft des öffentlichen Rechts"}
},
"analysis": { …Stefanie's 15 keys… },
"fetches": 5, "searches": 1, "checked_at": "<today YYYY-MM-DD>", "notes": "… or null"}
Resumability
A fresh session must be able to continue mid-campaign without re-fetching completed work:
- Pending detection is
company_idabsent fromenrichment.jsonl— nothing else. - Append after each supplier, never batch at the end. An interrupted run keeps its completed work.
- Write raw crawl shards per subagent (
enrichment.part-*.jsonl) and keep them as the evidence trail;map_ags.mjsmerges. - Do all work synchronously inside the run. Never spawn background agents and end your turn "waiting" — the run terminates the moment you produce your final message, killing anything still running (learned on netzbetreiber, 2026-08-04).
Finish
- Run
node campaigns/wasserversorger/map_roster.mjs(merges the crawl shards intoroster-links.jsonland resolves eachraw_nameto an AGS), thennode …/coverage.mjs(population coverage + the residual queue). Do not runmap_ags.mjs/export_csv.mjs— those are pilot-3-era scripts that read the deadcompanies.part-a/b.jsonl→companies.jsonl/links.jsonl/companies.csvpair (25 rows, frozen since 2026-08-12), not the liveenrichment.jsonl/roster-links.jsonldata. Running them is harmless (they just regenerate the same stale 25-row files) but wastes a step — multiple runs since 2026-09-03 have done this out of habit before catching it. There is currently no CSV export fromenrichment.jsonlfor the team's Sheet; that script does not exist yet. Persisted campaign scripts are.mjsso any session can run them.python3is available at/usr/bin/python3and is fine for throwaway analysis, but do not add Python to the campaign's committed toolchain. Rerunbuild_ags_map.mjswhenevernormNamechanges —ags-city-map.csvcachesname_norm, and a stale cache silently depresses the exact-match rate. - Journal entry (see country CLAUDE.md).
- Final message: batch size, per-field recovery counts, how many are complete vs needing outreach, municipalities newly claimed and population coverage (the metric that matters — not row count), plus anything odd worth a human's attention.