Skip to content

fix(xbrl): XbrlFactRepo.replaceForAccession no-ops on 0 rows unless intentionalClear#177

Closed
sroussey wants to merge 1 commit into
mainfrom
claude/wonderful-hypatia-q5iltz-h1-xbrl
Closed

fix(xbrl): XbrlFactRepo.replaceForAccession no-ops on 0 rows unless intentionalClear#177
sroussey wants to merge 1 commit into
mainfrom
claude/wonderful-hypatia-q5iltz-h1-xbrl

Conversation

@sroussey

@sroussey sroussey commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

XbrlFactRepo.replaceForAccession deletes any prior fact for the accession whose fact_index is not in the incoming set. When the incoming set is empty, that means every prior fact — a legitimate re-extract that yields 0 facts (parser regression, XBRL disabled, or a degrade path returning []) silently wipes the filing.

The sole production caller (src/sec/forms/registration-statements/s1/xbrlEnrichment.ts) early-returns NO_XBRL before touching the repo, so the wipe is not currently reachable in prod — but the invariant lives in the caller instead of the repo, and one refactor away from re-appearing.

Fix

Move the invariant into the repo:

  • replaceForAccession(accession_number, rows, opts?) — refuses a 0-row input with console.warn and returns. Callers that genuinely want to purge (an operator forcing a reset) pass { intentionalClear: true }.
  • clearForAccession(accession_number) — new explicit purge path.

Existing call sites all pass non-empty rows and need no changes; behavior is unchanged for them.

Tests

Added to src/storage/xbrl/XbrlFactRepo.test.ts:

  • no-ops (does not delete) when passed 0 rows without intentionalClear (spies console.warn, seeds 2 facts, asserts 2 remain).
  • clears prior facts when passed 0 rows with intentionalClear: true.
  • replaces prior facts on non-empty input (seeds 2, replaces with 2 different rows, asserts final state).
  • clearForAccession removes all facts for the target accession only.

Test plan

  • bun test src/storage/xbrl/ — 8 pass (4 new + 4 existing)
  • bun test src/sec/forms/registration-statements/s1/ — 72 pass
  • bun test src/cli/queries/XbrlQuery.test.ts — 5 pass
  • npx tsc --noEmit — clean

Generated by Claude Code

…ntentionalClear

A re-extract that legitimately yields 0 facts (parser regression, XBRL
disabled, or a degrade path returning `[]`) would otherwise wipe every
prior fact for the accession. The current sole caller
(`s1/xbrlEnrichment.ts`) early-returns `NO_XBRL` before touching the
repo, but the hole is one refactor away.

Move the invariant into the repo: `replaceForAccession` refuses a 0-row
input with a `console.warn` and returns, unless the caller opts in via
`{ intentionalClear: true }`. Add `clearForAccession` as the explicit
purge path for operator-driven resets.

Tests cover no-op-on-empty, opt-in clear, non-empty replacement, and
explicit clear.

sroussey commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Integrated into #176 (the consolidated hardening PR). The replaceForAccession 0-row guard + clearForAccession are carried forward verbatim (merged cleanly — no file overlap with #176's other changes), and bun test src/storage/xbrl/ passes (8) alongside the full suite there. Closing in favor of #176.


Generated by Claude Code

@sroussey sroussey closed this Jul 2, 2026
sroussey pushed a commit that referenced this pull request Jul 2, 2026
Correctness:
- parseDate: restore the literal year via setUTCFullYear before the calendar
  probe. Date.UTC remaps years 0-99 to 1900-1999, so a valid 4-digit year like
  '0099-01-01' was wrongly rejected as an invalid calendar date. Regression test
  added; Feb-30-style rollover detection is unaffected.

Resilience regression from #178's stricter parseDate throw:
- FetchQuarterlyIndexTask / FetchQuarterlyFormIdxTask: wrap the per-row
  secDate() in try/catch so a single calendar-invalid EDGAR 'Date Filed' row
  is skipped+warned instead of aborting the whole quarter's batch (parseDate
  now throws where it previously rolled forward).

Cleanup:
- XbrlFactRepo.clearForAccession delegates to replaceForAccession(acc, [],
  { intentionalClear: true }), removing a byte-identical duplicate delete loop.

Skipped (documented in review): the 4-way junction-repo copy-paste (would need
a shared CanonicalJunctionRepo base — larger refactor beyond this diff); the
CLI --date throw (operator fail-fast is acceptable); wiring clearForAccession
into xbrlEnrichment (pre-existing behavior #177 deliberately left unchanged);
and 5 concurrency observations that are pre-existing #175 reap-path limitations
or documented single-process scope, not wave-2 regressions.
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