fix(sec): SPAC writer atomicity, redemption LLM input cap, partial-success extractor outcome#170
Open
sroussey wants to merge 3 commits into
Open
fix(sec): SPAC writer atomicity, redemption LLM input cap, partial-success extractor outcome#170sroussey wants to merge 3 commits into
sroussey wants to merge 3 commits into
Conversation
SpacReportWriter.snapshot() derived valid_from from wall-clock next.updated_at and only de-collided against the currently-open history row, so clock-skew or a stale-replay could invert the chain or back-date a history snapshot. rebuild() and snapshot() also read-modify-write without any lock, so two concurrent writers on the same CIK could leave two valid_to == null rows. Anchor valid_from to the data: filingDate for non-stale writes, the existing row's as_of for stale replays, with strict monotonicity enforced against the max of all prior closed/open valid_to values. Wrap the rebuild critical section in withSpacCikLock — SQLite BEGIN IMMEDIATE, Postgres pg_advisory_xact_lock keyed on CIK, in-memory keyed mutex fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3e3m8cMRy5stFhDzGmZrF
processRedemption8K joined the primary doc + every EX-99 exhibit markdown unconditionally into runStructured, with MAX_TOKENS=4096 bounding only the model's completion. A multi-megabyte EX-99 ran up token bills and widened the prompt-injection surface proportional to filing size. Cap per-exhibit at 200k chars and total at 400k chars; oversized exhibits are dropped (not truncated, since a partial span breaks source-span verification). Full-drop records an OVERSIZED_INPUT dead-letter without invoking the model; partial-drop records an additional informational partial-letter so operators can triage filings whose largest exhibit was skipped. Bump redemption extractor version 1.0.0 -> 1.1.0 - the model now sees a different prompt shape, so confidence calibration drifts; treat as a fresh dev cycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3e3m8cMRy5stFhDzGmZrF
makeRunSection catches MODEL_INVALID_OUTPUT / LOW_CONFIDENCE_ALL / UNVERIFIED_SOURCE_SPAN, writes a dead-letter, and returns without throwing. ProcessAccessionDocFormTask then recorded a success extractor_run row even when every section dead-lettered, so sec version coverage counted them as covered and drop-previous purged the dead-letter rows operators needed for triage. Add a three-state outcome column (success / partial / failure) to extractor_runs. ProcessAccessionDocFormTask now queries the pending section-level dead-letters for the filing it just stored and writes outcome = partial when any exist. countSuccessfulAtVersion and listFilingsWithoutSuccessfulRun count only outcome = success; partial rows stay eligible for retry-dead-letters. Legacy rows backfill outcome from the existing success boolean - partial breakdown is unknowable for them; SQLite gets a one-shot ADD COLUMN migration in setupAllDatabases for pre-existing databases. Also tightens SpacWriteLock's backend dispatch to test the dealRepository class rather than the SEC_DB_TYPE token alone - tests register the token as sqlite while binding in-memory storages, so the env-only check spuriously opened a stray SQLite file via getDb(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3e3m8cMRy5stFhDzGmZrF
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.
Summary
Three correctness/security fixes stacked as separate commits.
1. SPAC writer atomicity + monotonic history chain
SpacReportWriter.snapshot()derivedvalid_fromfrom wall-clocknext.updated_atand only de-collided against the currently-open history row, so clock skew or a stale-replay could invert the chain or back-date a history snapshot.rebuild()andsnapshot()also read-modify-write without any lock, so two concurrent writers on the same CIK could leave twovalid_to == nullrows.valid_fromto the filing data (filingDatefor non-stale writes, the existing row'sas_offor stale replays) with strict monotonicity enforced against the max of all prior closed/openvalid_tovalues.withSpacCikLockwraps the rebuild critical section — SQLiteBEGIN IMMEDIATE, Postgrespg_advisory_xact_lockkeyed on CIK, in-memory keyed mutex fallback. Backend dispatch checks the active repo class so in-memory test backends never reachgetDb().SpacWriteLock.test.tsasserts exactly one open history row after 3 parallel writers on the same CIK.2. Cap redemption AI input bytes
processRedemption8Kjoined the primary doc + every EX-99 exhibit markdown unconditionally intorunStructured, withMAX_TOKENS=4096bounding only the model's completion. A multi-megabyte EX-99 ran up token bills and widened the prompt-injection surface proportional to filing size.OVERSIZED_INPUTdead-letter without invoking the model.<section>-partial-oversizeddead-letter so operators can triage filings whose largest exhibit was skipped.redemptionextractor1.0.0→1.1.0(prompt shape changed → confidence calibration drifts → fresh dev cycle, matching the S-1/424 precedent in PR fix: six HIGH-priority hardening fixes (prompt-injection seal + 8-K storage + XML entity expansion) #165).OVERSIZED_INPUTtoDEAD_LETTER_REASON_CODES.3. Partial-success outcome on
extractor_runsmakeRunSectioncatchesMODEL_INVALID_OUTPUT/LOW_CONFIDENCE_ALL/UNVERIFIED_SOURCE_SPAN, writes a dead-letter, and returns without throwing.ProcessAccessionDocFormTaskthen recorded asuccessrow even when every section dead-lettered, sosec version coveragecounted those as covered anddrop-previouspurged the dead-letter rows operators needed for triage.outcomecolumn (success/partial/failure) toextractor_runs.successboolean kept asoutcome === "success"for back-compat.ProcessAccessionDocFormTaskqueries pending section-level dead-letters for the filing and writesoutcome = "partial"when any exist.countSuccessfulAtVersionandlistFilingsWithoutSuccessfulRuncount onlyoutcome = "success"; partial rows stay eligible forretry-dead-letters.outcomefrom the existingsuccessboolean (partial breakdown is unknowable for them); SQLitesetupAllDatabasesgets a one-shotADD COLUMNmigration for pre-existing databases.PR #169 merge-order note
withSpacCikLockdoes NOT yet handle a nested transaction held by the caller. Onmaintoday,recomputeAndSaveDealsissues no innerBEGIN, so the SQLiteBEGIN IMMEDIATEhere is the only transaction in the rebuild stack. PR #169 (SpacDealReplace.ts) introduces its own transaction inrecomputeSpacDeals.If this PR lands first, PR #169 should rebase its transaction to either skip
BEGINwhen an outer lock holds it, or detect the active transaction and useSAVEPOINTinstead. If PR #169 lands first, this PR's SQLite path may need a similar guard.Test plan
bun run build— cleanbun test src/storage/spac/— all passbun test src/sec/forms/miscellaneous-filings/— all pass (incl. new oversized tests)bun test src/task/forms/— all passbun test src/storage/versioning/— all pass (incl. new partial-outcome tests)bun test src/cli/queries/— VersionCoverage tests still passbun test— 1386 pass / pre-existing FetchDailyIndexTask / FetchQuarterlyIndexTask network timeouts unrelated to this PRCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by Claude Code