Skip to content

MDM batches never fail on size at the end of a run - #906

Merged
paulananth merged 5 commits into
mainfrom
claude/merge-size-split
Oct 11, 2026
Merged

paulananth merged 5 commits into
mainfrom
claude/merge-size-split

Conversation

@paulananth

@paulananth paulananth commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

MDM batches never fail on size at the end of a run

The installed Company proof failed at the GLEIF merge: a match proposal of 19.7 MB, over mdm.record_match_proposal's 16 MB bound. The same failure happens on main with the old landed bundles, so it is not caused by the raw-capture work (#905). Size failures like this have recurred; this fixes the cause.

Why proposals were too big

How one SEC batch grows on its way through the Stage (919 records):

Stage Size Growth
Records file 1.4 MB x1
Save request 6.1 MB x4.4
Match proposal 9.6 MB x7

A GLEIF batch of 1,000 records (about 1.9 MB) made a 19.7 MB proposal.

  • The proposal held the save request twice: as command and as effects. effects is read only by save_batch's equality check.
  • Batches were cut by count (1,000), never by bytes.
  • The bounds were copied by hand into about 10 places and met only at the end, in the database.

What changes

  • Migration 013:
    • A proposal stores effects_sha256 in place of effects, and save_batch compares the request's hash to it.
    • A proposal stored before keeps its effects and is checked as before.
    • The 16 MB bound applies to the stored body; a proposal as sent may be 64 MB.
  • mdm.prepare cuts a batch at 1 MiB of records as well as at 1,000 records. Batch ids now count batches, so a re-prepared input that is byte-cut gets new ids.
  • The Merge Stage measures a batch's request before any SQL and refuses one over the bound, with its size and the remedy.
  • edgar_warehouse/mdm/clean/limits.py holds the bounds once; every Python site reads it, and a test holds the SQL functions to the same numbers.

Company preparation's own one-batch bundle gets the same byte cut on the L4 branch, which rewrites that file.

Evidence

  • New tests:
    • 6 Postgres proposal tests: the hash is stored and applies; an old-form proposal stored before 013 applies, and a changed request against one is refused; changed effects are refused; the bound applies to the stored body; an oversized request is refused before SQL.
    • 4 limits tests.
  • Locally:
    • 218 MDM Postgres integration tests (217 passed, 1 xfailed);
    • 822 MDM and engine tests;
    • 80 core Postgres tests after the review fixes.
  • Mastering time measured on chunk 1: 87% database, 13% Python. So the fix is structural; a Rust rewrite would change neither the time nor the failure.
  • Full installed proof on this branch: passed (2026-10-11 00:55 ET, 26 min). 6,414 Companies; GLEIF 3,716 records, 3,052 bound; bindings 9216cc18…, equal to the checkout replay; a killed worker's item verified on its next attempt; the second pass changed nothing. Report .planning/workstreams/mdm-batch-size/evidence/installed-company-proof-20261011.json.
    • Two earlier runs failed in the GLEIF read because the Mac slept mid-read and the worker's lease lapsed. The passing run kept it awake (caffeinate).

Review

Three axes:

  • GoF: leave it; plain constants plus a test, no pattern.
  • Standards: 013 correct against 001; names and messages fixed.
  • Spec: the early request check, the deferred-bound test and the old-form refusal test added.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

Aneena Ananth and others added 5 commits October 10, 2026 21:13
…place

A match proposal stored the whole save request twice, as `command` and
as `effects`, so it grew to about seven times its records and a GLEIF
batch's proposal reached 19.7 MB, over the 16 MB bound, at the end of a
long run. Migration 013 stores `effects_sha256` instead; save_batch
compares the request's hash to it, and a proposal stored before keeps
its effects and is checked as before. The 16 MB bound applies to the
stored body; a proposal as sent may be 64 MB.

mdm.prepare cuts a batch by a records byte budget (1 MiB) as well as by
1,000 records. The bounds live in `mdm/clean/limits.py`; every Python
site reads it and a test holds the SQL functions to the same numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk
The Stage measures a batch request before any SQL and refuses one over
the bound with its size and the remedy. Each limit has its own name;
messages read the constants; the deferred-record bound is tested; a
changed request against a proposal stored before 013 is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk
@paulananth
paulananth merged commit 9a72285 into main Oct 11, 2026
6 checks passed
paulananth pushed a commit that referenced this pull request Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk
paulananth pushed a commit that referenced this pull request Oct 11, 2026
A Company bundle held its up to 1,000 records in one batch (about 1.4 MB
for a real cm27 chunk). It is now cut by the same rule as mdm.prepare
(#906): at most limits.BATCH_RECORDS records and, past the first record,
limits.BATCH_RECORD_BYTES. The cut has one home, limits.batches(rows,
encode), used by both creators (GoF consult: extract one function, no
pattern).

The bundle writes records.{n}.jsonl, one consumer per bundle, batch ids
company:{bundle}:{n}, checkpoints n/n+1 and one publication key for every
batch. verify_company_bundle rebuilds the records from the workers and
compares every batch file with the same cut. The 1..1000 bound on the
documents list reads limits.BATCH_RECORDS.

Tests: the preparation tests read every batch; a new test cuts a bundle to
one record per batch and checks checkpoints, one consumer, one publication
key, verification and equal records; the limits tests call limits.batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paulananth added a commit that referenced this pull request Oct 11, 2026
* L4: the Company main reading compared over a whole capture, in shards

--limit 0 reads every receipt-pinned submission; --shard k/n splits them
across processes; each source.read unit removes its own artifacts once
verified. A 10-submission shard gives the same evidence as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: the whole capture's Company main reading is equal to the retained loader

All 76,230 pinned submissions, 76,230 Company rows and 8,065,661 filing rows,
every main table equal (4 shards, 17:51-18:43 ET). The ticket records the
landed-parquet finding and the remaining L4 parts; L6 closes with #904.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: per-CIK preparation costs and batch size checked

One Company on the raw path costs 0.57 s with a census read per chunk; mastering
one bundle as one batch or one record per batch gives identical results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: the refined plan for Name Frequency and preparation from the raw capture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4 (in progress): Name Frequency and Company preparation read the raw capture

Name Frequency counts a population of main submissions documents named by
receipt key, each read from the capture through the new census-raw.yaml
(two a unit) and checked against its receipt; the switched-off cascade is
refused when on. Company preparation reads its bundle's documents, the
census and the exchange catalog through census-main, census, catalog and
combine-census, every worker verified and retained. Both commands take the
capture in place of landing manifests.

Still in progress: the landed preparation tests are being rewritten for the
raw capture; the whole-population census and chunk comparisons are running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: Name Frequency from the raw capture equals cm27; approvals recorded

The raw census equals cm27's census.json on 43,338 filers and 43,245
entries (entries, GLEIF, normalizers and version equal; 24 minutes).
The SEC capture fixture moves to tests/support/sec_capture.py. The
operator approved the Company main and census-main readings and the
Dataset Contract (2026-10-10 19:25 ET).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: verify every combine worker; chunk 1 masters as the landed bundle

verify_company_bundle re-built records from the last combine worker
only; it now re-builds them from every unit's combine output, with each
record's bronze origin, through the same helpers preparation uses.
cm27 chunk 1 from the raw capture: 966 rows equal to the landed ones;
mastered, 919 Companies, 919 bindings, 47 reviews, golden fields equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: chunks 2-7 equal from the raw capture; bundle size finding

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: Company preparation tests read the raw capture

Fourteen tests prepare from a receipt-bound raw capture; the landed
preparation tests and their landed oracle are removed (equivalence is
held by the recorded whole-capture and chunk evidence). The installed
caller test prepares from a raw capture too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: the installed preparation caller passes on the raw capture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: retire company_source.py and the landed Company contracts

The SEC Company governance constants live in mdm/clean/sec_company.py,
read by preparation and the tests. company_source.py, the five landed
contracts, the landed qualification scripts and the landed oracle tests
are deleted; the operations spec and the data-onboarding skill describe
the raw-capture preparation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4 scratch: the Rust question research note and the mastering timing harness

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4 ticket: preparation creator, comparable rows ticked; the configured pipeline deferred to 20c

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4 ticket: census-raw read approved; rebased onto #906

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4 ticket: GoF consult for the Company batch cut; next steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFKdv4nqKXKM9d5Cf2hEFk

* L4: Company preparation cuts its bundle as mdm.prepare does

A Company bundle held its up to 1,000 records in one batch (about 1.4 MB
for a real cm27 chunk). It is now cut by the same rule as mdm.prepare
(#906): at most limits.BATCH_RECORDS records and, past the first record,
limits.BATCH_RECORD_BYTES. The cut has one home, limits.batches(rows,
encode), used by both creators (GoF consult: extract one function, no
pattern).

The bundle writes records.{n}.jsonl, one consumer per bundle, batch ids
company:{bundle}:{n}, checkpoints n/n+1 and one publication key for every
batch. verify_company_bundle rebuilds the records from the workers and
compares every batch file with the same cut. The 1..1000 bound on the
documents list reads limits.BATCH_RECORDS.

Tests: the preparation tests read every batch; a new test cuts a bundle to
one record per batch and checks checkpoints, one consumer, one publication
key, verification and equal records; the limits tests call limits.batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* L4 ticket: the batch cut implemented and tested; landed provenance names deferred to the next SEC mapping

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* L4 review fixes: one raw-capture reader, the manifest bound in limits, dead code

Three-axis review (Standards, Spec, GoF) of #905:
- The raw capture was read twice (receipts, the bronze key check, the CIK
  from a key, the execute-then-verify of a source.read unit), and Company
  preparation imported Name Frequency's private helpers. One module,
  mdm/clean/raw_capture.py, now holds them; both callers use it.
- Company preparation refuses a batch file over limits.RECORDS_FILE_BYTES
  and a manifest over limits.MANIFEST_BYTES, as mdm.prepare does. The 32 MB
  manifest bound, written in three places, is limits.MANIFEST_BYTES.
- Dead code: the unused SourceRejected import and CATALOG_BYTES, a wrong
  noqa, write_name_frequency's unused policy parameter. ZERO is named
  CENSUS_DIGEST; the population bound is a named constant.
- The custom-parsing experiment compared configuration with
  company_source.py and gleif_source.py, both retired; it is removed and the
  dated results note says so. The source-contract spec cites source.yaml
  for the identifier example; the CLI help no longer says 1..1000; the
  local-operations doc names the capture folder one way.

Equal output: six real Companies prepared with f1508d9 and with this
commit into the same folder give the same bundle byte for byte. 82
preparation, Name Frequency, limits, worker-artifact and engine tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* L4 ticket: the three-axis review and its fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* L4 ticket: the 7 bundles re-prepared with the cut, equal records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* L4: the installed proof on the raw-capture bundles passes; ticket closed

6,414 Companies; GLEIF 3,716 records, 3,052 bound; bindings 9216cc18, equal to the checkout replay; recovery on attempt 2; the second pass changed nothing (26.5 min). Report evidence/installed-company-proof-raw-20261011.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* whole-source-census ticket: the installed population proof is done

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Aneena Ananth <aneenaananth@MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@paulananth
paulananth deleted the claude/merge-size-split branch October 15, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant