# SEC fundamentals alerts, 2026-08-21 Two `sec_facts` warnings, investigated against live SEC data. Both originate in SEC's own published data — a stale per-company Company-Facts file (1) and a stale ticker→CIK mapping (2) — and neither is a parser defect: no stored fundamental value is wrong. Every SEC-side probe below reproduces offline from public endpoints; the four database facts used are quoted where they appear. ## 1. `filing_gap_aged` — 43 gaps, all `not_in_companyfacts` **Root cause: SEC's per-company Company-Facts files are stale for these issuers, while the same filings are present in SEC's own `frames` aggregation.** All ten named filings are real 10-Qs filed 2026-07-28/29, present in the issuer's `submissions` with `isXBRL=1`, with complete R-files and XBRL in the EDGAR archive — and absent from `companyfacts/CIK*.json`: | CIK | issuer | accession | filed | in `companyfacts` | newest fact in file | |---|---|---|---|---|---| | 0000001800 | Abbott | 0001628280-26-050134 | 2026-07-28 | no | 2026-04-29 | | 0000021344 | Coca-Cola | 0001628280-26-050503 | 2026-07-29 | no | 2026-04-30 | | 0000024741 | Corning | 0000024741-26-000255 | 2026-07-29 | no | 2026-05-01 | | 0000029989 | Omnicom | 0000029989-26-000019 | 2026-07-29 | no | 2026-04-29 | | 0000037996 | Ford | 0000037996-26-000156 | 2026-07-29 | no | 2026-04-30 | | 0000040533 | General Dynamics | 0000040533-26-000032 | 2026-07-29 | no | 2026-07-01 | | 0000048898 | Hubbell | 0001628280-26-050405 | 2026-07-29 | no | 2026-06-04 | | 0000049071 | Humana | 0000049071-26-000050 | 2026-07-29 | no | 2026-04-29 | | 0000049196 | Huntington Bancshares | 0000049196-26-000066 | 2026-07-28 | no | 2026-04-30 | | 0000062996 | Masco | 0000062996-26-000027 | 2026-07-29 | no | 2026-04-22 | Ruled out, with evidence: - **Not a global SEC outage.** Company Facts is current for other issuers filing the same days — MSFT `0001193125-26-323660` @2026-07-29, AAPL @2026-07-31, P&G @2026-08-04, Chevron @2026-08-06, JPMorgan @2026-08-20. - **Not a CDN/cache artifact.** A cache-busted request with `Cache-Control: no-cache` returns the identical stale 3.39 MB payload; the response carries no cache headers. - **Not our filter.** The scan covers every taxonomy/concept/unit in the payload. - **Not a metadata discriminator.** Gap and non-gap filings are identical on `isXBRL`, `isInlineXBRL`, `reportDate`, `primaryDocDescription`. - **SEC does have the facts.** `frames/us-gaap/Assets/USD/CY2026Q2I.json` lists Abbott at exactly the missing accession `0001628280-26-050134`, and Coca-Cola and Ford at theirs. The per-company endpoints are the degraded ones: `companyconcept/CIK0000001800/us-gaap/Assets.json` returns `"units":{"USD":{}}`. **Consequence, and why the gate changed.** Retrying `companyfacts` cannot recover these — Abbott's file has been stale since April. And because `active_gaps` supersedes a gap only on a *successfully ingested later* filing, a stale file also swallows Q3: the pause was open-ended, not seasonal, on 43 large caps. **Fix** (`app/services/fundamentals_quality_service.py`): once `filing_gap_aged` has escalated a gap (`escalated_at`), it stops pausing setups **if** the issuer's own newest stored 10-K/10-Q is under `GAP_GATE_RECENT_FILING_DAYS` (180) old. Pause hands off to the alert; an issuer with nothing that recent stays paused. `active_gaps` is deliberately untouched, so `_retry_backlog` keeps retrying and a recovered filing still resolves normally. The bound is applied to the queue path *and* the `validation_json` summary path, which mirrors the same filings — bounding only one leaves the behaviour unchanged in production. **This is a bounded reprieve, not a removal — know the two ways it ends.** Abbott's newest ingested filing is `0001628280-26-028357`, filed 2026-04-29, so its recency window closes around **2026-10-26**; most of the 43 sit on late-April filings and turn back to paused within days of each other. That crossing is **silent**: the importer escalates only gaps with `escalated_at IS NULL`, so `filing_gap_aged` does not re-fire for a gap it has already reported. Separately, a Q3 10-Q that also fails to ingest creates a *new* un-escalated gap on the same CIK, which re-pauses it at once (that one does raise its own `filing_gap_aged` 14 days later). Whether the silent re-block deserves a re-escalation signal is an open call, deliberately not made here — "one actionable escalation rather than a daily warning" is the existing design intent. **Not done, with reasons.** A `frames`-backed recovery source was considered and rejected: frames are calendar-aligned with a tolerance (off-fiscal filers drop out) and carry one fact per issuer per period, so amendment/restatement semantics differ from Company Facts — lossy as a snapshot source, not merely expensive. Parsing the filing's own inline-XBRL instance is the authoritative alternative but is a new subsystem (contexts, dimensions, unit refs) duplicating the parser's fact model. ## 2. `snapshot_discrepancy` — 0000906107-15-000012 / -000016 **Root cause: two tracked tickers claim the same filing, because SEC's `company_tickers.json` still points the old symbol at a non-traded co-registrant. No stored value is wrong and no reparse is warranted.** CIK 0000906107 is **Vivmark Residential** (VMRK, formerly Equity Residential). Both alerted accessions are **combined EQR + ERP Operating LP 10-Qs** — one accession, two registrants (0000906107 and 0000931182) — the pattern behind the existing co-registrant recovery path. The stored rows are **byte-identical** to what the current parser reconstructs from EQR's own Company Facts — every column, verified: `cik` (`0000906107`), `form`, `filed_date`, `accepted_at`, both period dates, `fiscal_year`/`fiscal_period`, `revenue`, `net_income`, `operating_income`, `diluted_eps`, `cfo`, the two nulls, `cash_and_st_investments`, `total_debt` (340,900,000 / null), `shares_outstanding`, `shares_outstanding_date`, `weighted_avg_diluted_shares`. Both carry `import_run_id = 6`, and CIK 0000906107 holds all 69 of its filings across runs 6–30, so the issuer's own history is complete. Run 63 (2026-08-19) recorded `fields: ["cik"]` for both accessions, and the universe explains it: ``` tickers: VMRK -> 0000906107 (Vivmark Residential, ex-Equity Residential) EQR -> 0000931182 (ERP Operating Ltd Partnership) ``` SEC's own `company_tickers.json` carries `{"cik_str": 931182, "ticker": "EQR", "title": "ERP OPERATING LTD PARTNERSHIP"}` — after the rename, the old symbol stayed attached to the **non-traded operating partnership**, the co-registrant on those combined 10-Qs. `resolve_ciks` reads `active_only` tickers and follows SEC, so 0000931182 is tracked. Its Company Facts holds 7 accessions, exactly 2 of them EQR-prefixed, so its backfill reconstructs exactly those two rows, stamps them `cik=0000931182`, and collides with the rows already stored under 0000906107 — identical in every fact, differing only in attribution. It cannot self-heal. The collision loser never stores a row (the insert is skipped as immutable), so `_ciks_with_snapshots` never sees 0000931182, and it is full-history backfilled — refetching every submissions shard and its companyfacts — **on every run**, re-raising the warning each time. `fundamental_snapshots` for 0000906107 holds all 69 filings across runs 6–30, so the issuer's own history is complete and correct. ### Fixes **Code** (`sec_fundamentals_importer.py`): a `cik`-only difference is no longer reported as a reconstruction discrepancy. It raises `accession_cik_collision`, naming both CIKs and pointing at `sec_cik_overrides`, because the fix is the universe, not the parser. The reparse path also excludes these from its rewrite set — rewriting a cik-only difference would re-stamp the filing onto the co-registrant and take it from the issuer that filed it. (A reparse run while both CIKs are tracked fails validation on `duplicate accession in staged snapshots` instead, which is a safe stop.) **Data — needs an operator, and the alert repeats daily until then.** `EQR` is a stale symbol: the security now trades as `VMRK`, which is already tracked at the correct CIK. Retiring the `EQR` ticker ends the loop. A `sec_cik_overrides` pin of `EQR -> 906107` would silence the collision but leave two tickers on one security, double-counting the issuer in scans — retirement is the right action. **Not fixed, deliberately:** the permanent-backfill loop itself. A tracked CIK whose only parseable filings belong to another CIK is re-backfilled every run; ending that in code means teaching `_ciks_with_snapshots` about foreign-owned accessions, which is more state for a condition that is now loudly and specifically reported. ### Separate observation: `total_debt` on this issuer looks wrong Independent of the alert, and unchanged by any fix here: the parser reconstructs `total_debt = 340,900,000` for EQR's 2015 Q1 and `null` for Q2, while the REIT carried roughly $10bn of debt. `_compose_debt` returns the short-term component alone when every `_LONG_TERM_DEBT_AGG` concept **and** the `LongTermDebtNoncurrent`/`Current` pair miss — which is what happened here, and Q2 matched neither. Worth checking against a current REIT filer before trusting `total_debt` for that sector. --- ## 3. Follow-ups from the two alerts above ### 3a. The reprieve in (1) ended silently — now it doesn't The hand-off in section 1 is a **bounded** reprieve. It ends two ways, and neither said anything: the issuer's stored filings age past `GAP_GATE_RECENT_FILING_DAYS` (for the 43, their last good filings are late April, so ~2026-10-26), or a newer filing gap arrives and the all-escalated condition fails. `filing_gap_aged` cannot report either, because it only escalates gaps whose `escalated_at` is NULL and so never fires twice for the same gap. `sec_filing_gaps.exempted_at` (migration `034`) makes the transition observable: set quietly while the issuer is exempt, cleared when the exemption lapses, and the clear is what raises `filing_gap_repaused`. Once per lapse, re-arming if the issuer's data recovers and ages out again. A gap that was never exempt has no transition and stays silent — it is simply still paused, which `filing_gap_aged` already said. The exemption rule itself is not duplicated: `fundamentals_quality_service.gap_exempt_ciks` is now public and the importer alerts on membership changes in exactly the set the gate reads. ### 3b. `total_debt` was materially wrong for a third of large caps The EQR observation in section 2 was not a REIT edge case. Measured over 19 large caps, the old composition — `LongTermDebt`, else `LongTermDebtNoncurrent`/`Current`, plus one of `ShortTermBorrowings`/`CommercialPaper` — missed two whole tagging styles: | issuer | before | after | what was missed | |---|---:|---:|---| | T | None | 143.95b | `LongTermDebtAndCapitalLeaseObligations` | | XOM | None | 47.66b | same | | VZ | 21.78b | 165.23b | same (read only the current maturities) | | KO | 0.25b | 39.31b | same (read only commercial paper) | | HD | 3.50b | 48.33b | same | | O | 1.40b | 26.53b | REIT parts (`NotesPayable` + `SecuredDebt`) | | VMRK | 1.50b | 9.09b | same | | CVX | 0.40b | **None** | partial suppressed — see below | | PFE | 63.10b | 63.19b | `DebtCurrent` is the completer current side | | 10 others | — | unchanged | already composed correctly | `total_debt` feeds `net_debt` → `net_debt_to_ebitda` → the peer percentile and the categorical leverage read, so Coca-Cola at 0.25bn of debt was not a missing value — it was a confident *"conservative leverage"* on an issuer carrying ~39bn. The composition now spans four mutually exclusive styles, with each concept's span respected: `LongTermDebt` already includes current maturities (Apple tags all three and 71.34 + 11.01 = 82.30 confirms it), `LongTermDebtAndCapitalLeaseObligations` is noncurrent and needs a current complement, and `DebtCurrent` *is* that whole complement rather than an addition to it. **A short-term component alone is no longer reported as a total.** Chevron tags full debt only in its 10-K, so its 10-Q carries 0.40bn of short-term borrowing and nothing else. `_net_debt` needs both sides and yields nothing when either is missing, so None costs a leverage read where the partial value produced a confidently wrong one. The REIT branch needed disambiguating, because `NotesPayable` does not mean the same thing across issuers (measured over 14 REITs): MAA tags `NotesPayable` 5.66bn = `UnsecuredDebt` 5.30bn + `SecuredDebt` 0.36bn **exactly**, so there it is the total and adding the secured side double-counts — while EQR tags it alongside a *larger* `SecuredDebt` (5.38bn vs 6.38bn in 2013), where it is only the unsecured component. `UnsecuredDebt`'s presence separates the two: where tagged it is the unambiguous unsecured side and `NotesPayable` is ignored; where absent, `NotesPayable` is that side. Both sides are required, which is also what stops the branch inventing a total from a fragment. | REIT | before | after | | |---|---:|---:|---| | MAA | None | 5.66b | matches its own `NotesPayable` total exactly | | KIM | None | 8.74b | | | O / VMRK | 1.40b / 1.50b | 26.53b / 9.09b | | | BXP | 0.75b | **None** | tagged only `SecuredDebt` + paper against ~15bn real debt | | VTR | 0.27b | **None** | same shape | | 8 others | — | unchanged | already composed correctly | Known limit: where EQR tags both the parts and the aggregate, the parts sum 2.6–12.2% *below* it, so this branch approximates. It is last in line — any issuer tagging an aggregate never reaches it — and the alternative there is no value at all. ### Sequencing the history fix Snapshots are immutable, so **3b corrects new filings only**; every stored quarter keeps its old `total_debt`. `scripts/reparse_fundamentals.py` exists for exactly this ("after a parser fix, keeping the stored row is preserving a stale cache"). **Retire the `EQR` ticker before reparsing.** A reparse backfills every tracked CIK, so while both 0000906107 and 0000931182 are tracked, both stage the same two 2015 accessions and the run fails validation on `duplicate accession in staged snapshots`. That is a safe stop — nothing is written — but the reparse will not complete until the collision is gone.