Skip to content

Never retract a proxy event on a merger-proxy run that reached no verdict - #317

Merged
sroussey merged 1 commit into
mainfrom
claude/zen-albattani-alr17v-proxy-retraction
Aug 21, 2026
Merged

Never retract a proxy event on a merger-proxy run that reached no verdict#317
sroussey merged 1 commit into
mainfrom
claude/zen-albattani-alr17v-proxy-retraction

Conversation

@sroussey

Copy link
Copy Markdown
Contributor

The bug

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 MODEL_RESOLUTION_ERROR, RATE_LIMITED, NONCE_MISMATCH, LOW_CONFIDENCE_ALL
and the MODEL_INVALID_OUTPUT catch-all all arrive as an unset extractedDeal
indistinguishable from "a 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. The deterministic half of the evidence (seeks_combination_approval) had
been computed in that same run and still read true, but it was discarded because it is only
persisted 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 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.

Worse, it repeated. No extraction row is written on the failure branch, so
seeks_combination_approval stayed NULL and mergerProxyDescriptor.filterTodo's null-verdict
clause 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) is
exactly where it fires.

The fix

Both writes now require evidence about the document.

  • recordMergerProxy takes a tri-state ProxyEventVerdict (emit / retract / leave),
    resolved by the pure resolveProxyEventVerdict. A general definitive statement retracts on
    the deterministic seeks_combination_approval === false — conjunctive with the deal, so it
    decides 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_ALL and
    UNVERIFIED_SOURCE_SPAN, where the model did return a deal and only its certainty or its
    citation failed.
  • runSection returns a SectionOutcome (skipped / persisted / dead-lettered + reason)
    so its caller can tell a verdict from a failure. Every other caller ignores it.
  • The deterministic verdict is recorded on an existing extraction row even when the run
    extracted nothing (SpacMergerExtractionRepo.recordApprovalVerdict): the gate really was
    evaluated, 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 model
    resolves at all) leaves a recorded proxy event standing, with the failure still pending on
    the 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 — the resolveProxyEventVerdict matrix.
  • sectionRunner.test.ts — the outcome a run reports for MODEL_EMPTY vs the
    MODEL_INVALID_OUTPUT catch-all vs SECTION_NOT_FOUND, plus persisted / skipped.

Verification

  • bunx vitest run over proxies-information-statements, storage/spac, task/forms,
    registration-statements: 92 files, 896 tests passed.
  • Full bun run test: 3701 passed, 20 skipped, 1 failed
    parseOfferingTables.corpus.test.ts > loads committed S-1 fixtures, a golden-corpus parse
    that 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 it
prevents.


🤖 Generated with Claude Code

https://claude.ai/code/session_01Ske1Jwk7fDFxHykfZGEzce


Generated by Claude Code

`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
@sroussey
sroussey merged commit 657f7ea into main Aug 21, 2026
1 check passed
@sroussey
sroussey deleted the claude/zen-albattani-alr17v-proxy-retraction branch August 21, 2026 15:30
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.

2 participants