Skip to content

Mastering writes a batch once: outbox by batch, Company fields once, no repeated write - #907

Merged
paulananth merged 10 commits into
mainfrom
claude/mdm-request-dedup
Oct 11, 2026
Merged

paulananth merged 10 commits into
mainfrom
claude/mdm-request-dedup

Conversation

@paulananth

@paulananth paulananth commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Mastering writes a batch once

Ticket: .planning/workstreams/mdm-request-dedup/TICKET.md.

Root cause

Each step that handled a batch kept its own whole copy of it, and a batch with identity work was written twice. On cm27 chunk 1 (919 Companies, a 1.4 MB records file), one batch wrote about 36 MB into the store:

Fact Copies before Where
Each source reading 5 source_reading, stage_record.reading, batch.effects, the journal outbox row, the match proposal
Each golden record 7 company.body, company.fields, batch.effects, the journal outbox row twice, the graph and export outbox rows
Each decision 4 decision, batch.effects, the journal outbox row, the proposal

The write itself ran twice: preview_batch (all of write_batch, rolled back), then save_batch.

What

  • 014, outbox by batch. A new outbox row keeps only the hash of what it sends. What it sends is the view mdm.outbox_delivery, built from the batch row. It is a view because migrate revokes every function from PUBLIC and a publisher's verifier holds SELECT only. A row stored before keeps and sends its own payload.
  • 015, Company fields once. company.fields was body->'fields' again; company_master_field reads body, and the column is dropped.
  • The write runs once. MergeStage.apply with identity work assesses once (no preview write, no second computation), commits the proposal before any master write (Q13), then saves the computed batch after re-checking its snapshot and each rule proposal under the lock. assess/apply_assessment keep their preview for a proposal made on its own.

Not done, per the GoF consult (reasons on the ticket): the Stage's copy of the winning reading, per-field policy digests, the publication key per reading.

Evidence

Before After
Mastering chunk 1 11.3–11.4 s 6.6 s (−42%)
Store after one batch 36.2 MB 16.5 MB (−54%)
Outbox 5.4 MB under 0.2 MB
company 5.1 MB 1.9 MB
  • New tests: test_clean_outbox_names_batch.py, test_clean_company_fields_once.py, test_clean_assess_and_apply.py. Each migration is tested on rows written by the 001 functions. The built outbox payload and hash equal what the 001 write stored, for journal, export and graph.
  • 239 MDM Postgres tests pass (1 xfail).

Full proof (passed)

The installed empty-store proof on the 7 raw-capture cm27 bundles, 12:47–13:14 ET 2026-10-11: 6,414 Companies; GLEIF 3,716 records, 3,052 bound; bindings 9216cc18…, equal to the checkout replay and to #905's proof on the same bundles; recovery on attempt 2; the second pass changed nothing. SEC merge 4.0 to 2.8 min, GLEIF merge 1.9 to 1.3 min against #905's proof. Report .planning/workstreams/mdm-request-dedup/evidence/installed-company-proof-20261011.json.

Three-axis review done; its two faults (an unrelated batch between assessment and save; a refusal at save) fixed and tested in 3024c745. 241 MDM Postgres tests pass.

🤖 Generated with Claude Code

paulananth pushed a commit that referenced this pull request Oct 11, 2026
…t save is recorded, one rule-proposal check

Spec review: an unrelated batch saved between the assessment and the save
made the save fail on its fixed generation; the spec says an unrelated
generation alone does not invalidate a proposal. The save now takes the
current generation under the lock (the effects hash leaves it out), after the
snapshot and rule-proposal checks. A batch PostgreSQL refuses at save now
keeps a rejected assessment, as one refused when assessed does.

Standards and GoF: the policy, check_policy and rule-proposal re-check are
one method used by _execute and the save (the save had skipped the missing
policy check); recording a proposal in a caller's transaction is
assessment.record_in; the load-bearing `except StaleAssessment: raise` says
why. candidate-assessments.md describes the new apply path.

Tests: an unrelated batch saved between assessment and save; a refusal at
save. test_clean_assess_and_apply.py 5, the concurrent-correction race 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paulananth
paulananth marked this pull request as ready for review October 11, 2026 17:18
Aneena Ananth and others added 10 commits October 11, 2026 13:43
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
One batch of 919 Companies (1.4 MB of records) writes about 35 MB: each step keeps its own whole copy (readings 5 times, golden records 7, decisions 4), and the write runs twice (preview, then save).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no repeated write

Root cause (ticket): each step that handles a batch kept its own whole copy,
and a batch with identity work was written twice (preview, then save).

- 014: an outbox row keeps the hash of what it sends, not a copy. What a
  consumer is sent is built from the batch row (outbox_payload_of); a row
  stored before keeps and sends its own payload (outbox_payload). The new
  hash equals the hash the 001 write stored, for every consumer.
- 015: company.fields was body->'fields' a second time; the
  company_master_field view reads body, and the column is dropped. Versions
  stored before read the same after.
- MergeStage.apply with identity work: the batch is assessed once (no
  preview_batch write, no second computation), its proposal committed before
  any master write as before (Q13), then saved in the next transaction after
  re-checking the snapshot and each rule proposal under the lock. A refused
  batch keeps a rejected proposal; a stale one is superseded and re-proposed.
  `assess` and `apply_assessment` keep their preview for a proposal made on
  its own.

Tests: test_clean_outbox_names_batch.py (3), test_clean_company_fields_once.py
(2), test_clean_assess_and_apply.py (3); readers moved to outbox_payload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ace test hooks the save step

migrate revokes every mdm function from PUBLIC (the fence), so a verifier login, which holds SELECT only, could not call the payload function. What a row sends is now the view mdm.outbox_delivery; write_batch hashes it, claim_outbox and the publishers read it. The concurrent-correction test injects its race at _save_assessed, the step apply now uses. 239 MDM Postgres tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t save is recorded, one rule-proposal check

Spec review: an unrelated batch saved between the assessment and the save
made the save fail on its fixed generation; the spec says an unrelated
generation alone does not invalidate a proposal. The save now takes the
current generation under the lock (the effects hash leaves it out), after the
snapshot and rule-proposal checks. A batch PostgreSQL refuses at save now
keeps a rejected assessment, as one refused when assessed does.

Standards and GoF: the policy, check_policy and rule-proposal re-check are
one method used by _execute and the save (the save had skipped the missing
policy check); recording a proposal in a caller's transaction is
assessment.record_in; the load-bearing `except StaleAssessment: raise` says
why. candidate-assessments.md describes the new apply path.

Tests: an unrelated batch saved between assessment and save; a refusal at
save. test_clean_assess_and_apply.py 5, the concurrent-correction race 1.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paulananth
paulananth force-pushed the claude/mdm-request-dedup branch from 1f4bdd9 to 41f5c3f Compare October 11, 2026 17:44
@paulananth
paulananth merged commit 4ace81d into main Oct 11, 2026
6 checks passed
paulananth added a commit that referenced this pull request Oct 11, 2026
Ticks the last part with the green head and merge commit.

Co-authored-by: Aneena Ananth <aneenaananth@MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@paulananth
paulananth deleted the claude/mdm-request-dedup branch October 11, 2026 18:00
paulananth added a commit that referenced this pull request Oct 11, 2026
* Skill audit: skills state today's Company and GLEIF status

data-onboarding, refining-rules, READING.md and COMBINING.md no longer call
the installed proof, whole-capture equivalence and the older readers' deletion
open (#901, #904, #905, #907); they name what is open (in-process Company
preparation, adapters.py, capture). Prose says Name Frequency, file names
stay. The handoff's finished lines are ticked with evidence.
data-platform/SKILL.md waits for Codex's responsibility-map branch.

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

* Skill audit: review fixes; one open-work list

READING.md and COMBINING.md: remaining census prose says Name Frequency,
stale status passages state today, a dated 2026-10-11 qualification
section, the retired builder and reader named. The open-work list lives in
data-onboarding; refining-rules points to it (GoF finding).

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>
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