Never retract a proxy event on a merger-proxy run that reached no verdict - #317
Merged
Merged
Conversation
`recordMergerProxy` took a boolean, and `processMergerProxy` computed it as `extractedDeal && seeks_combination_approval === true` for the general definitive statements — deleting the accession's `proxy` event whenever that was false. But `runSection` contains every model and transport failure as a dead letter and returns normally, so an unresolved model, a provider throttle or the `MODEL_INVALID_OUTPUT` catch-all all arrive as an unset `extractedDeal`, indistinguishable from "the model read the section and found no deal". A run that merely could not reach a model therefore retracted a `proxy` event an earlier successful run had recorded from real evidence. Losing that event takes the whole approval stage with it: the vehicle's next Form 25/15 inside the 90-day post-approval window then classifies `deregistration` instead of `completed`, and `recordDeregistration` deletes the `completed` event — a genuinely de-SPAC'd vehicle recorded as a wind-up, with `surviving_name` / `post_merger_*` / the `current_*` promotion dropping back to the SPAC-era mirror. The documented recovery ceremony (`sec extractor backfill merger-proxy`) is exactly where this fires, and it repeated on every sweep: no extraction row is written on the failure branch, so `seeks_combination_approval` stayed NULL and the descriptor's null-verdict clause re-selected the same filing forever, contrary to its own "self-extinguishing" contract. Both writes now require evidence about the DOCUMENT. `recordMergerProxy` takes a tri-state (`emit` / `retract` / `leave`) resolved by `resolveProxyEventVerdict`: a general definitive statement retracts on the deterministic `seeks_combination_approval === false` — conjunctive with the deal, so it decides alone and keeps the ceremony working during a provider outage — or on a dead letter that is itself an answer (`SECTION_NOT_FOUND` / `MODEL_EMPTY`). Everything else leaves the event stream untouched, including `LOW_CONFIDENCE_ALL` and `UNVERIFIED_SOURCE_SPAN`, where the model did return a deal and only its certainty or its citation failed. `runSection` returns a `SectionOutcome` so its caller can tell those apart; every other caller ignores it. To make the backfill converge, the deterministic verdict is now recorded on an existing extraction row even when the run extracted nothing: the gate really was evaluated, and it is a property of the document rather than of the model call. No row is invented where none exists — every predicate downstream reads an extraction row as "this proxy produced something". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ske1Jwk7fDFxHykfZGEzce
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
recordMergerProxytook a boolean, andprocessMergerProxycomputed it asextractedDeal && seeks_combination_approval === truefor the general definitivestatements — deleting the accession's
proxyevent whenever that was false.But
runSectioncontains every model and transport failure as a dead letter and returnsnormally, so
MODEL_RESOLUTION_ERROR,RATE_LIMITED,NONCE_MISMATCH,LOW_CONFIDENCE_ALLand the
MODEL_INVALID_OUTPUTcatch-all all arrive as an unsetextractedDeal—indistinguishable from "a model read the section and found no deal". A run that merely could
not reach a model therefore retracted a
proxyevent an earlier successful run had recordedfrom real evidence. The deterministic half of the evidence (
seeks_combination_approval) hadbeen computed in that same run and still read
true, but it was discarded because it is onlypersisted inside
persist, which never ran.Losing that event takes the whole approval stage with it: the vehicle's next Form 25/15 inside
the 90-day post-approval window then classifies
deregistrationinstead ofcompleted, andrecordDeregistrationdeletes thecompletedevent — a genuinely de-SPAC'd vehicle recordedas a wind-up, with
surviving_name/post_merger_*/ thecurrent_*promotion dropping backto the SPAC-era mirror.
Worse, it repeated. No extraction row is written on the failure branch, so
seeks_combination_approvalstayed NULL andmergerProxyDescriptor.filterTodo's null-verdictclause re-selected the same filing on every sweep — contrary to its own "self-extinguishing"
contract, and the documented recovery ceremony (
sec extractor backfill merger-proxy) isexactly where it fires.
The fix
Both writes now require evidence about the document.
recordMergerProxytakes a tri-stateProxyEventVerdict(emit/retract/leave),resolved by the pure
resolveProxyEventVerdict. A general definitive statement retracts onthe deterministic
seeks_combination_approval === false— conjunctive with the deal, so itdecides alone and keeps the recovery ceremony able to unwind a stale close during a provider
outage — or on a dead letter that is itself an answer (
SECTION_NOT_FOUND/MODEL_EMPTY).Everything else leaves the event stream untouched, including
LOW_CONFIDENCE_ALLandUNVERIFIED_SOURCE_SPAN, where the model did return a deal and only its certainty or itscitation failed.
runSectionreturns aSectionOutcome(skipped/persisted/dead-lettered+ reason)so its caller can tell a verdict from a failure. Every other caller ignores it.
extracted nothing (
SpacMergerExtractionRepo.recordApprovalVerdict): the gate really wasevaluated, and it is a property of the document rather than of the model call, so the
backfill's null-verdict clause converges. No row is invented where none exists — every
predicate downstream reads an extraction row as "this proxy produced something".
No extractor version bump: persisted extraction rows are unchanged, and the derived event is
rebuildable from the document with no model call.
Tests
New, each named for the invariant it holds:
Form_DEFM14A.storage.e2e.test.ts— a re-run whose extraction fails (and one where no modelresolves at all) leaves a recorded
proxyevent standing, with the failure still pending onthe worklist; a false approval verdict still retracts even when the extraction failed; the
verdict is recorded when nothing was extracted, and the merger-proxy backfill descriptor then
returns no candidates for that filing. The first three fail on
main.Form_DEFM14A.proxyVerdict.test.ts— theresolveProxyEventVerdictmatrix.sectionRunner.test.ts— the outcome a run reports forMODEL_EMPTYvs theMODEL_INVALID_OUTPUTcatch-all vsSECTION_NOT_FOUND, plus persisted / skipped.Verification
bunx vitest runoverproxies-information-statements,storage/spac,task/forms,registration-statements: 92 files, 896 tests passed.bun run test: 3701 passed, 20 skipped, 1 failed —parseOfferingTables.corpus.test.ts > loads committed S-1 fixtures, a golden-corpus parsethat takes ~22 s alone against a 30 s timeout and passes on its own here; unrelated to this
change (this box was running other suites concurrently).
bun run format-check:All matched files use Prettier code style!bun run build: clean (bundle +tsc).CLAUDE.md's "Retraction" section is updated with the tri-state rule and the cascade itprevents.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ske1Jwk7fDFxHykfZGEzce
Generated by Claude Code