Skip to content

Serve filings from the local SEC corpus when edgar_search found them - #2990

Merged
bxyu-nvidia merged 1 commit into
mainfrom
ude/sec-corpus-lookup-fix
Sep 3, 2026
Merged

bxyu-nvidia merged 1 commit into
mainfrom
ude/sec-corpus-lookup-fix

Conversation

@ushnish-de

Copy link
Copy Markdown
Contributor

Serve filings from the local SEC corpus when edgar_search found them

edgar_search queries a local full-text index built from a downloaded SEC
corpus, but reads of its results did not come from that corpus. To turn a filing
URL into a file on disk, _lookup_dump rebuilt a path out of _filings_cache,
which only sec_filing_search populates. After an edgar_search hit there was
nothing to rebuild from, so the read went to sec.gov.

When sec_filing_search had run earlier in the same rollout, the rebuilt path
was worse than useless for exhibits β€” the separate documents filed alongside a
filing, such as an EX-99.2 press release. A rebuilt path can only ever name a
filing's main document, so the read returned that instead of the exhibit, and
reported success.

Changes

  • edgar_search records each hit's source_path from the index, keyed by
    canonical URL, and filing reads consult it before falling back to the rebuilt
    path. The index holds a row per document, so this is the only route that can
    name an exhibit.
  • Filing reads are counted by source (cache, sec-corpus, live). The totals
    are logged on the first read, then at most once every 30 minutes, and on
    shutdown, at warning level so they appear in job logs with no logging
    configuration.

Metrics

The fix changes how a filing URL is turned into a file path. There are two ways
to do that, and the table compares them on the same documents:

  • the exact path the index recorded for the document the search returned
  • a path rebuilt from filing metadata, which can only name a filing's main
    document

8 queries against a 3-year, 5-ticker demo index returned 138 distinct documents
(62 exhibits, 76 main documents). Each was read both ways, and the bytes that
came back were compared against the document the search had actually returned:

how the file path is chosen right document wrong document no file found
exact path from the index β€” after this change 138 (100%) 0 0
rebuilt from filing metadata β€” before this change 64 (46%) 56 (41%) 18 (13%)

All 56 wrong reads were requests for an exhibit, where we previously returned the filing's primary document instead of the exhibit itself. None of the 76 primary-document reads is wrong, which is the shape the mechanism predicts: a rebuilt path can only ever name primary-document.html, so it is right for a filing's own document and wrong for every exhibit in that filing.

@copy-pr-bot

copy-pr-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@ushnish-de ushnish-de changed the title Serve filings from the local SEC corpus when edgar_search found them Draft: Serve filings from the local SEC corpus when edgar_search found them Sep 2, 2026
- `_lookup_dump` was unreachable after `edgar_search`, because it rebuilds paths from `_filings_cache`, which only `sec_filing_search` fills.
- `edgar_search` now records each hit's `source_path` from the index, and filing reads use it before falling back to the filing-layout convention.
- The convention can only name `primary-document.html`, so on exhibits it returned the filing's primary document and reported success: 56 of 62 exhibits across 8 demo queries.
- Counts filing reads by source (cache, sec-corpus, live) and logs the totals on the first read, then at most once every 30 minutes, and at process exit, since every step in that chain falls back silently.

Signed-off-by: Ushnish De <ude@nvidia.com>
@ushnish-de
ushnish-de force-pushed the ude/sec-corpus-lookup-fix branch from c2e75f1 to dce9d45 Compare September 3, 2026 02:19
@ushnish-de ushnish-de changed the title Draft: Serve filings from the local SEC corpus when edgar_search found them Serve filings from the local SEC corpus when edgar_search found them Sep 3, 2026
@cmunley1

cmunley1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

/ok to test dce9d45

@cmunley1

cmunley1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

/claude review

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

SHIP β€” LGTM, no reliability concerns.

Reviewed all 4 changed files (finance_sec_search serving path + tests). This adds a search-driven dump-path index so edgar_search hits can be read from the local SEC corpus (including exhibits, which the convention-based path could never name) instead of hitting sec.gov, plus a per-source read counter.

Why this is low-risk:

  • No verifier/scoring surface touched. This only changes where filing text is sourced. Dump, cache, and live all return the same document, so a bug here degrades to a live fetch or the existing convention path β€” never a wrong score.
  • Correct document mapping. _dump_paths is keyed on canonical_url_key(url) and populated from the index row matching that exact URL key, so a recorded path can only ever serve the document its URL requested. Missing dump files fall through to live (covered by test_missing_dump_file_falls_through_to_live).
  • Path traversal guarded. _lookup_dump rejects absolute paths and .. components before reading under sec_dump_path.
  • Async-safe. SQLite reads go through asyncio.to_thread / run_in_executor; no httpx, no ray.get, no missing awaits. The _dump_paths dict is only mutated at await boundaries in the single-threaded loop; concurrent edgar_search calls at worst redo identical resolves.
  • Backward compatible. supports_dump_paths degrades cleanly on indexes built before the source_path/canonical_url_key columns existed (test_dump_paths_are_unavailable_without_the_columns). New imports are stdlib only.
  • Well tested. New tests exercise precedence, exhibit serving, live fallback, and the read-source counter.

One soft coupling worth noting (not blocking): canonical_url_key must stay byte-identical to the index builder's key normalization (unpadded CIK / dashless accession / lowercased filename). If they ever diverge, lookups silently miss and fall back to convention/live β€” degradation, not corruption. The code comment already flags this; keeping the builder and this function in sync is the maintenance obligation.

if text_content is not None:
return text_content

# Otherwise rebuild the path from filing metadata that sec_filing_search

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a recorded document path exists but can’t be read, should we return None here instead of falling back? For exhibit URLs, the fallback below reads primary-document.html, so a missing exhibit can silently return the wrong document.

@bxyu-nvidia
bxyu-nvidia merged commit c50eb20 into main Sep 3, 2026
38 checks passed
@bxyu-nvidia
bxyu-nvidia deleted the ude/sec-corpus-lookup-fix branch September 3, 2026 03:44
cmunley1 pushed a commit that referenced this pull request Sep 3, 2026
…annot name (#3046)

Follow-up to #2990. Thanks @cmunley1 for catching this in review β€” the
answer
is yes, and the same gap turns out to be reachable without any recorded
path
at all.

### The two ways a URL becomes a file

A filing on EDGAR is a folder of documents: one primary document, plus
exhibits such as an `EX-99.1` press release, each with its own URL. To
serve a
read from the local corpus, `_lookup_dump` has two mechanisms.

The **recorded path** comes from the index, which holds a row per
document, so
it can name an exhibit. #2990 added it and gave it precedence.

The **layout convention** rebuilds
`ticker/form/year/accession/primary-document.html`
from filing-level metadata. Because that metadata is per filing rather
than per
document, this path can only ever name the primary document β€” but it
cannot tell
whether the URL it was handed is that document, so it returns text and
reports
success either way.

The convention predates the index by three months (#1304, May) and was
written
when `sec_filing_search` was the only search. Every URL that tool emits
is built
from the submissions API's `primaryDocument`, so the convention was
correct by
construction: a filing-level search feeding filing-level URLs into a
filing-level
path rebuild. `edgar_search` (#2722) widened the input domain from
filings to
documents, and the fallback kept the old assumption.

### Why the fallback cannot just check where the URL came from

`_lookup_dump` receives a URL and nothing else. Holding a recorded path
for it is
the only provenance signal available, and it is a proxy that goes
missing while
the URL really did come from `edgar_search`:

- `_record_dump_paths` is deliberately non-fatal, so results reach the
model
  while the paths silently go unrecorded
- an index built before the `source_path` / `canonical_url_key` columns
returns
  `{}` for every lookup
- `canonical_url_key` drifting from the builder's normalization misses
silently
- nothing constrains the read tool's input to a URL that appeared in a
search
  result

In each case the URL is indistinguishable from one that was never
searched, and
the convention resolves if `sec_filing_search` ran for that CIK earlier.
Note
`_filings_cache` is process-level and disk-backed, so "earlier" spans
rollouts,
not just the current one.

### Changes

- Decline the convention when the URL names a document other than the
filing's
`primary_document`. This reasons about the URL rather than its
provenance, so
  it holds in all four cases above.
- Stop falling through when a recorded path exists but no readable file
does,
  and warn: the index and the corpus have drifted apart.
- Both return `None`, which routes to the live fetch β€” the correct
document, one
  request slower. Neither guard fails a read.

One deliberate limit: when the metadata carries no primary-document
name, the
first guard does not fire. The URL's document cannot be classified then,
and
declining on a guess would push reads back to sec.gov, which is what the
corpus
exists to avoid.

### Evidence

Three tests, run against `main` with only `app.py` reverted:

| test | on c50eb20 | with this change |
| --- | --- | --- |
| exhibit whose recorded file is missing | fails β€” returns the filing's
primary document, counted `sec-corpus=1 live=0` | passes β€” live fetch,
`sec-corpus=0 live=1` |
| exhibit URL with no recorded path | fails β€” returns the filing's
primary document | passes β€” declines, live fetch |
| the filing's own document via the convention | passes | passes |

The third is the no-regression case, and it passing on both sides is
what shows
the other two are testing behaviour rather than restating the new code.
Full
component suite: 106 passed.

Signed-off-by: Ushnish De <ude@nvidia.com>
jkyi-nvidia pushed a commit that referenced this pull request Sep 11, 2026
…2990)

## Serve filings from the local SEC corpus when edgar_search found them

`edgar_search` queries a local full-text index built from a downloaded
SEC
corpus, but reads of its results did not come from that corpus. To turn
a filing
URL into a file on disk, `_lookup_dump` rebuilt a path out of
`_filings_cache`,
which only `sec_filing_search` populates. After an `edgar_search` hit
there was
nothing to rebuild from, so the read went to sec.gov.

When `sec_filing_search` had run earlier in the same rollout, the
rebuilt path
was worse than useless for exhibits β€” the separate documents filed
alongside a
filing, such as an `EX-99.2` press release. A rebuilt path can only ever
name a
filing's main document, so the read returned that instead of the
exhibit, and
reported success.

### Changes

- `edgar_search` records each hit's `source_path` from the index, keyed
by
canonical URL, and filing reads consult it before falling back to the
rebuilt
path. The index holds a row per document, so this is the only route that
can
  name an exhibit.
- Filing reads are counted by source (`cache`, `sec-corpus`, `live`).
The totals
are logged on the first read, then at most once every 30 minutes, and on
  shutdown, at warning level so they appear in job logs with no logging
  configuration.

### Metrics

The fix changes how a filing URL is turned into a file path. There are
two ways
to do that, and the table compares them on the same documents:

- **the exact path the index recorded** for the document the search
returned
- **a path rebuilt from filing metadata**, which can only name a
filing's main
  document

8 queries against a 3-year, 5-ticker demo index returned 138 distinct
documents
(62 exhibits, 76 main documents). Each was read both ways, and the bytes
that
came back were compared against the document the search had actually
returned:

| how the file path is chosen | right document | wrong document | no
file found |
| --- | --- | --- | --- |
| exact path from the index β€” after this change | 138 (100%) | 0 | 0 |
| rebuilt from filing metadata β€” before this change | 64 (46%) | 56
(41%) | 18 (13%) |

All 56 wrong reads were requests for an exhibit, where we previously
returned the filing's primary document instead of the exhibit itself.
None of the 76 primary-document reads is wrong, which is the shape the
mechanism predicts: a rebuilt path can only ever name
primary-document.html, so it is right for a filing's own document and
wrong for every exhibit in that filing.

Signed-off-by: Ushnish De <ude@nvidia.com>
Signed-off-by: Junkeun Yi <jkyi@nvidia.com>
jkyi-nvidia pushed a commit that referenced this pull request Sep 11, 2026
…annot name (#3046)

Follow-up to #2990. Thanks @cmunley1 for catching this in review β€” the
answer
is yes, and the same gap turns out to be reachable without any recorded
path
at all.

### The two ways a URL becomes a file

A filing on EDGAR is a folder of documents: one primary document, plus
exhibits such as an `EX-99.1` press release, each with its own URL. To
serve a
read from the local corpus, `_lookup_dump` has two mechanisms.

The **recorded path** comes from the index, which holds a row per
document, so
it can name an exhibit. #2990 added it and gave it precedence.

The **layout convention** rebuilds
`ticker/form/year/accession/primary-document.html`
from filing-level metadata. Because that metadata is per filing rather
than per
document, this path can only ever name the primary document β€” but it
cannot tell
whether the URL it was handed is that document, so it returns text and
reports
success either way.

The convention predates the index by three months (#1304, May) and was
written
when `sec_filing_search` was the only search. Every URL that tool emits
is built
from the submissions API's `primaryDocument`, so the convention was
correct by
construction: a filing-level search feeding filing-level URLs into a
filing-level
path rebuild. `edgar_search` (#2722) widened the input domain from
filings to
documents, and the fallback kept the old assumption.

### Why the fallback cannot just check where the URL came from

`_lookup_dump` receives a URL and nothing else. Holding a recorded path
for it is
the only provenance signal available, and it is a proxy that goes
missing while
the URL really did come from `edgar_search`:

- `_record_dump_paths` is deliberately non-fatal, so results reach the
model
  while the paths silently go unrecorded
- an index built before the `source_path` / `canonical_url_key` columns
returns
  `{}` for every lookup
- `canonical_url_key` drifting from the builder's normalization misses
silently
- nothing constrains the read tool's input to a URL that appeared in a
search
  result

In each case the URL is indistinguishable from one that was never
searched, and
the convention resolves if `sec_filing_search` ran for that CIK earlier.
Note
`_filings_cache` is process-level and disk-backed, so "earlier" spans
rollouts,
not just the current one.

### Changes

- Decline the convention when the URL names a document other than the
filing's
`primary_document`. This reasons about the URL rather than its
provenance, so
  it holds in all four cases above.
- Stop falling through when a recorded path exists but no readable file
does,
  and warn: the index and the corpus have drifted apart.
- Both return `None`, which routes to the live fetch β€” the correct
document, one
  request slower. Neither guard fails a read.

One deliberate limit: when the metadata carries no primary-document
name, the
first guard does not fire. The URL's document cannot be classified then,
and
declining on a guess would push reads back to sec.gov, which is what the
corpus
exists to avoid.

### Evidence

Three tests, run against `main` with only `app.py` reverted:

| test | on c50eb20 | with this change |
| --- | --- | --- |
| exhibit whose recorded file is missing | fails β€” returns the filing's
primary document, counted `sec-corpus=1 live=0` | passes β€” live fetch,
`sec-corpus=0 live=1` |
| exhibit URL with no recorded path | fails β€” returns the filing's
primary document | passes β€” declines, live fetch |
| the filing's own document via the convention | passes | passes |

The third is the no-regression case, and it passing on both sides is
what shows
the other two are testing behaviour rather than restating the new code.
Full
component suite: 106 passed.

Signed-off-by: Ushnish De <ude@nvidia.com>
Signed-off-by: Junkeun Yi <jkyi@nvidia.com>

This branch was previously deployed

1 inactive deployment
public β€” dce9d45e Deployed Sep 3, 2026 by copy-pr-bot[bot] via release / finalize / notify #2478
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.

3 participants