Skip to content

fix(spac): stop counting partial extractions as issuer failures - #283

Merged
sroussey merged 3 commits into
mainfrom
claude/keen-knuth-hxoj5s-process-exit
Aug 14, 2026
Merged

fix(spac): stop counting partial extractions as issuer failures#283
sroussey merged 3 commits into
mainfrom
claude/keen-knuth-hxoj5s-process-exit

Conversation

@sroussey

Copy link
Copy Markdown
Contributor

Stacked on #282 (which is stacked on #281). Merge order: #281#282 → this. GitHub will retarget to main as each lands.

Defect (MEDIUM) — sec spac process reports "issuer(s) failed" and exits non-zero for routine partial extractions

src/commands/spac.ts counted a row toward the exit code on row.partial > 0 || row.failed > 0, then threw ${failed} of ${parsed.length} issuer(s) failed.

But partial is the documented normal outcome when one AI section dead-letters — the commit that introduced the summary cites a DRS whose underwriters section came back MODEL_EMPTY. It is counted from persisted extractor_runs rows with outcome === "partial". It is not an issuer failure.

Concrete failure

sec spac process 1234567 on a healthy SPAC prints:

1234567: 51/52 filings (2020-09-23 → 2023-10-03); 1 partial
⚠ Some sections did not extract. Inspect them with: sec extractor dead-letters <extractor-id>
Error: 1 of 1 issuer(s) failed

and exits 1. Since almost every real SPAC has at least one dead-lettered section, the non-zero exit is the default, so any script or CI step gating on it treats a normal run as a failure — including the repair pass in #281, which runs sec spac process over every known SPAC.

Approach

Extract the decision into an exported pure helper, next to spacProcessRows / formatSpacProcessSummary which are already exported and unit-tested:

export function spacProcessFailureCount(rows: readonly ProcessSpacTimelineTaskOutput[]): number {
  return rows.filter((row) => row.error !== "" || row.failed > 0).length;
}

Per-row printing is unchanged — a partial still prints the same warn line pointing at sec extractor dead-letters. Only the counter and the message change; the message now reads N of M issuer(s) had failed filings, which is what it now means.

Rejected alternative

Keeping partials in the count but downgrading the exit to a warning: the exit code is the whole signal here, and a "warning exit code" is not a thing a CI step can act on differently. Making the helper pure and exported also puts the rule under unit test, which the inline let failed = 0 counter never was.

Tests

New describe("spacProcessFailureCount") in src/commands/spac.test.ts. All five were written first and confirmed failing (TypeError: spacProcessFailureCount is not a function):

  • does not count a partial-only issuer{ partial: 1, failed: 0, error: "" } → 0. The finding's failure.
  • counts an issuer with failed filings{ failed: 2 } → 1.
  • counts an issuer whose replay errored{ error: "boom" } → 1.
  • counts a failed issuer once and excludes the partial ones — mixed batch of three rows → 1.
  • counts an issuer that is both partial and failed only once → 1.

Verification

  • npx vitest run src/commands → 6 files, 35 tests passing.
  • npx tsc --noEmit clean; bun run build clean.

Risk

Strictly fewer non-zero exits. A CI step that deliberately gated on partials loses that signal — the warn line and sec extractor dead-letters <extractor-id> remain the surface for it. No migration.

Must land before the #281 repair pass, since that pass runs sec spac process over every known SPAC and would otherwise exit 1.


🤖 Generated with Claude Code


Generated by Claude Code

`sec spac process` incremented its failure counter for `row.partial > 0`
and then threw "N of M issuer(s) failed", exiting non-zero. A partial run
is the documented NORMAL outcome when a single AI section dead-letters,
and almost every real SPAC has at least one — so a healthy replay printed
"1822912: 51/52 filings (...); 1 partial" and exited 1, and any script or
CI step gating on the exit code treated it as a failure.

The decision moves into an exported `spacProcessFailureCount`, counting a
row only when the replay errored or a filing actually failed. The warn
line for partials is unchanged, and the message now says "had failed
filings" rather than "failed".

A CI step that deliberately gated on partials loses that signal; the warn
line and `sec extractor dead-letters <id>` remain the surface for it.
@sroussey
sroussey changed the base branch from claude/keen-knuth-hxoj5s-dereg-order to main August 14, 2026 17:23
@sroussey
sroussey merged commit 02594af into main Aug 14, 2026
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