Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…ers, bankruptcies, payouts) 31 EDGAR-verified cases in data/golden_events.csv; scripts/build_golden_fixtures.py captures what the resolver and classifier read for each into tests/fixtures/golden/; tests/golden.py replays it offline. test_golden_events.py strict-xfails the 24 cases today's code gets wrong, each mapped to the plan task that should fix it. TickerResolver gains an unused keyword-only member_names= (read from Task 3 on); pytest now imports this checkout (pythonpath = src, .). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…icker-free name match, proxy window) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…ded plan HLTH's state-court receivership (8-K 1.03, 2019-09-24) 14 days after the delisting decides its terminal value. IMCL has no automatic path, so the golden test pins it as production does (GOLDEN_MANUAL); it now classifies 573 via REVOKED and waits only for Task 3's flag. XFAIL_BUCKET follows the review rulings: HLTH -> 4, XTO -> 6, KCI -> 9, reasons updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
… first, two-word name match, ±30-day names) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
SEC refusals (403/429) were silently absorbed by broad `except Exception` clauses in the ticker resolver and recorded as "No CIK found" — exactly what happened from 2026-05-28 to 2026-09-16. Adds `EdgarBlocked` and `check_response()` in edgar.py, called after every SEC HTTP response in edgar.py and the EFTS helpers in ticker_resolver.py; `except EdgarBlocked: raise` guards in front of the broad excepts so a refusal always propagates. classify_universe.py re-raises per-ticker and aborts the process (exit 2) instead of writing an error row. A resolver miss (cik=None) is no longer persisted to the cache, and TickerResolver.__init__ skips loading any pre-existing cache entry with cik=None, so misses recorded by older/blocked runs get retried instead of being trusted forever. Adds an autouse `_no_efts_network` fixture (tests/conftest.py) so the suite no longer makes live efts.sec.gov calls in test_unknown_ticker_returns_unknown, which the sandbox was silently swallowing — the same failure class this fix targets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…pt blocks Task-2 fix round 1. PayoutExtractor.extract and LLMMergerTermsExtractor.extract already let EdgarBlocked propagate cleanly (they only catch requests.RequestException), but scripts/classify_universe.py's per-ticker payout- and llm-terms-extraction try/excepts caught it via their bare `except Exception as e:`, printed "payout ERROR" / "llm-terms ERROR", and kept going — the same refusal-read-as-a-miss failure this task exists to close. Adds `except EdgarBlocked: raise` in front of both, matching the guard already in place for classify_ticker(). Covering tests in tests/test_edgar_blocked.py assert PayoutExtractor and LLMMergerTermsExtractor let EdgarBlocked propagate out of extract() when a fake EDGAR's fetch_filing_text raises it for a merger record with a closing 8-K (so extraction reaches the text fetch instead of short-circuiting). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…ember/company mismatch Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…own Form 25; replay raw EFTS in golden tests - _efts_lookup returns (cik, name, fallback); a disagreeing second-pass hit is a fallback that the name search may replace only with a Form 25/15 within 90 days, else kept as efts_name_mismatch - _expected_name skips names with no usable word (AT&T, 3M); _name_search ranks with name_tokens - golden fixtures gain efts_raw (both EFTS answers, keyed by URL); tests/golden.py serves them to the real EFTS methods; build_golden_fixtures.py records them and has --efts-only Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…onfirmed 1.03 tag is ignored A company that kept filing periodic reports after emerging from Chapter 11 with the same CIK (Oasis Petroleum) was misread as an exchange transfer, because the "continued filings" override ran before any bankruptcy check. A merger 8-K's Item 1.03 tag with no bankruptcy text and no Item 1.03 section in the body (Vista Outdoor) was taken at face value and misread as a bankruptcy. - evidence.bankruptcy_8ks(filings, on, before=540, after=30): 8-Ks tagged 1.03 within the window, oldest first. - evidence.mentions_bankruptcy(text): "bankruptcy" / "chapter 11" / "chapter 7" / "receivership", case-insensitive. - classifier._confirmed_bankruptcy: the first windowed 1.03 8-K whose text confirms it (an empty/fetch-miss text counts as confirmed — the tag is SEC's own metadata). Runs right after the SEC-REVOKED check and before the continued-filings exchange-transfer override, so a real bankruptcy on record always wins. - classifier._effective_items: strips a 1.03 tag from an anchor 8-K's item set when its text is fetched and does not mention a bankruptcy, flagging "bankruptcy_tag_unconfirmed" (deduped) so the merger/compliance fingerprint underneath can classify normally. Golden harness: deletes the Task 4 XFAIL_BUCKET entries (OAS, MDR, WE, LYLT, VSTO, HLTH); all six now pass for real. SPWR (already passing) now resolves through the bankruptcy check with reason "Bankruptcy (8-K item 1.03 filed 2024-08-06)". Suite: 246 passed, 13 xfailed, 3 pre-existing warnings (up from baseline 236 passed / 19 xfailed: +4 new unit tests, +6 golden cases moved from xfail to pass). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
… flag frozen vendor tails _pick_delist_filing now returns (filing, gap_days) and gates a Form 25 candidate more than FORM25_TAIL_DAYS (45) before the observed date: it is rejected unless the nearest/backscanned 8-K carries an M&A item (2.01/5.01/ 3.03) or no operating filing (10-K/10-Q/20-F/40-F, or an earnings 8-K) appears between the Form 25 and the observed date. This stops an unrelated older Form 25 (SKLZ's 2021 warrant delisting, a stale candidate >1500 days out) from anchoring a later event. A Form 25 that is accepted despite a gap > 45 days is flagged frozen_tail:<days> (evidence["anchor_gap_days"] records the gap for every case). New pure helper evidence.filed_operating_between. Golden harness: 18 passed, 13 xfailed, unchanged — KCI and XTO now carry frozen_tail (370 and 955 days) as their golden fixtures expect, but both still need a later task's item-set/evidence rule to reach the right bucket, so neither XFAIL_BUCKET entry moves. SKLZ's Form 25 is now correctly rejected (delist_filing is None) but it still needs Task 7's rename/ listing-transfer handling, so it stays mapped there too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…2.01, and beats 2.04 Takeover closing 8-Ks often carry items 3.01+3.03+5.01 but no 2.01 (C.R. Bard, Rohm & Haas), and some carry 2.04 because notes are put on the change in control (Onyx). Both used to fall through to compliance failure (570) or distress (470), producing a fake -100%/-90% training label. `_classify_items` now recognizes a "control" fingerprint -- 5.01 plus any of 2.01/3.01/3.03 -- as 231 (M&A change in control, closing 8-K without 2.01), checked before the plain 2.01+3.01 and 2.04+3.01 rules so it wins over an incidental 2.04. Deletes the Task 6 XFAIL_BUCKET entries (BCR, ONXX, XTO), which now classify correctly. Confirmed SPWR still passes via Task 4's bankruptcy check, which runs before _classify_items. All other golden cases are unchanged (verified by diffing the full 31-id table before and after). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…s, not compliance failures LendingClub -> Happen (NYSE -> Nasdaq) and Skillz -> Firy both filed a 3.01 notice and kept reporting results; today they fall through to a 570 compliance failure, which downstream becomes a fake -100% training label. Add evidence.renamed_near, still_operating, item_text, and says_listing_transfer, and check them in classify_ticker right after the confirmed-bankruptcy rule and before the continued-filings override, so a frozen Form 25 tail (SKLZ's 2021 SPAC-merger Form 25) can't hide it. still_operating (needs a Form 15) is what keeps BLD's merger-time rename to "QXO Insulation, LLC" out of this rule. Delete the Task 7 XFAIL_BUCKET entries for LC and SKLZ; both now pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
Code review found _rename_or_transfer fires on renamed_near alone: a
company acquired through a standard 2.01+3.01+5.01 merger, renamed at
closing, with registered debt still filing 10-Qs and no Form 15, came
back as EXCHANGE_TRANSFER (304) instead of MERGER (231).
Per the controller's ruling, decline the rule whenever an 8-K within
+/-30 days of the observed date (report_date falling back to
filing_date, as elsewhere in the classifier) shows the registrant was
acquired: item 5.01, or 2.01 together with 3.01 or 3.03. A bare 2.01
(a disposition) does not block it. The merger fingerprint then decides
via the existing _classify_items path.
Also: _TRANSFER_TEXT now matches past tense ("transferred its
listing"), not just present tense.
Golden suite unchanged: 23 pass, 8 xfail; LC, SKLZ, HYH still pass;
BLD stays merger.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…ot distress A blank-check company (SPAC) that liquidates its trust redeems shares at trust value (~$10, true return ~0). Today such delistings land in compliance_failure or liquidation, which downstream becomes a fake -100%/-90% training label. Add evidence.is_spac(sub, on): true when the EDGAR SIC is 6770 or the name at the delisting date matches /acquisition\s+corp/i. In classify_ticker, check it after the rename/transfer rule and before the continued-filings override (only when a Form 25 or dereg is on record), emitting flag "spac" and CRSP 600/EXPIRATION. Golden harness: FST, BWC, HMA, LEAP move from XFAIL to passing (all carry spac + member_name_mismatch). PEAK and LBRDA/LBRDK, whose resolved companies are not SPACs, are unaffected. A pre-existing unit test (test_resolver_member_names.py) asserted an exact flags list for the FST/FAST-Acquisition-Corp case; updated it to include the new "spac" flag, which is correct now that the classifier can see it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…t a bare SIC hit Review finding: is_spac ORed the SIC-6770 test unconditionally, so a de-SPAC whose EDGAR SIC was never updated from 6770 (EDGAR doesn't always update it after the merger) would still read as a SPAC. Since the SPAC branch runs before the Form-25/8-K fingerprint, a later real merger or compliance failure under the same CIK got pre-empted into a false EXPIRATION/600 (dlret 0). Ruling: is_spac(sub, on) is True when the name at `on` matches "Acquisition Corp", or when the SIC is 6770 AND no formerNames entry has a `to` date on or before `on` (no completed rename by `on`). A de-SPAC always renames; the four real SPAC liquidations in the golden set (FST, BWC, HMA, LEAP) all have SIC 6770 and empty formerNames, so they are unaffected. Docstring rewritten to state the exact rule and the reason. Golden suite unchanged: 27 passed, 4 xfailed (FST/BWC/HMA/LEAP still pass). Full suite: 275 passed, 4 xfailed, 3 pre-existing warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…a closing 8-K are found by their proxy/tender filings Every no-fingerprint return and every 3.01-alone 570 (Form 25 path and 8-K-only path) now goes through _default_without_fingerprint: 2.01 + Form 15 -> merger 233; 3.01 text citing a listing deficiency -> 570/580; merger proxy/tender filing (window from the Form 25) -> merger 231; NT 10-K/Q alone -> 580; else unknown + no_evidence_default with evidence["deregistered"]. enrich renders an unknown deregistration with a valid last close at par (ASSUMED_PAR). SIAL, UTIW, AABA, KCI pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…tandard exchange deficiency wording - item_text finds the Item heading in any case (ITEM 3.01 notices were read as empty, so a real deficiency fell through to unknown at par). - _DEFICIENCY_TEXT adds: failure to comply with / satisfy the|its|one or more continued listing, abnormally low, average global market capitalization, no longer suitable for (continued) listing, commence(d) proceedings to delist. The standard 3.01 heading alone still does not match. - The helper flags notice_text_missing when a 3.01 anchor's text is empty. - Tests: uppercase heading, the phrase table, both 580 branches (NT window from the Form 25), text-missing flag, exact codes on the merger tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
… elections are mixed, ties abstain Veritiv's "$170 in cash" (whole dollars, no decimals) was missed while a "$1.00 multiplied by ... PBU award" figure was read instead; Two Harbors' $25 preferred redemption won a tie over the $12.00 common cash; TopBuild's cash-or-stock election was read as all cash. Fixes: - _AMT now accepts whole dollars or exactly two decimals (still refuses a truncated 4-decimal figure); _AMT_LONG reads a long decimal only after "cash payment of" (CPWR's $10.389188). - _CLASS_CONTEXT/_AWARD_AFTER skip a figure that belongs to another security class (preferred redemption) or an award payout. - _MIXED_AFTER also joins on "or" (a cash-or-stock election) with an optional lettered/roman list marker; _MIXED_WINDOW widened to 120 to span BLD's "(the Cash Consideration) or (ii) 20.200 shares". - _select now abstains on a tie between two comparably-sized figures (TWO) instead of always preferring the larger value. - classify_universe.py writes payout values as %.6g so long decimals survive. Golden payout test added (test_golden_payout, parametrized over merger cases with expected_dlret); XFAIL_PAYOUT holds only BLD (a genuine cash-or-stock election, resolved by Task 11's price check). VRTV, TWO, CPWR, ONXX, SIAL, UTIW, KCI all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…ecision, tie-branch regression test Applies three controller rulings on top of 1660886: 1. test_golden_payout now asserts pr.value is not None before computing the implied return, dropping the "abstain -> 0.0 fallback" that let VRTV_2023-11-30 pass the golden test even when the extractor found no match at all (pre-Task-10). All 8 PAYOUT_CASES now require a real extraction; BLD_2026-07-01 stays in XFAIL_PAYOUT (strict) and still xfails on its mixed-election abstain. 2. Added test_match_cash_plus_cvr_equal_count_tie_still_takes_cash: unlike the existing APLS test (cash 2x beats CVR 1x via the higher-count path), this repeats the CVR figure to force an equal-count tie ($41.00 x2 vs $4.00 x2) and asserts _select still returns the cash floor because the CVR is under 25% of the cash — exercising the tie-but-small-second-value branch the 25%-abstain rule was written for. 3. classify_universe.py: both payout CSV writes changed .6g -> .10g so CPWR's $10.389188 (8 significant digits) survives verbatim instead of rounding to $10.3892. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…by the leg the price reconciles with Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…r-deal cash, gated payouts.csv - payout_gate.gate_payouts: the post-loop routing as a pure, tested function - an election where both legs fit takes the leg nearer the last close (tie: stock) - LLM cash is accepted for any non-election terms without a stock ratio (cash + CVR "other") - full cash+stock terms clear their key's payout_gate_failed flags - payouts.csv is written after the gate from the gated values - election stock rows keep payout_source llm_election_stock - DEFAULT_TOL shared by the CLI and the golden test - builder: lazy script import, skip LLM capture without --raw-tiingo-dir, snapshot-relative raw_tiingo_dir Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
… the rules could not settle - EnrichedDelistRecord.review_flags joins evidence["flags"] with enrich()'s new extra_flags, plus a distress_at_normal_price flag when a compliance/liquidation bucket's last trade closed at $5+. - review_flags is appended (last) to DLRET_TABLE_COLUMNS and enriched_to_row; build_dlret_table takes a new payout_flags map to source extra_flags. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
… flag; wire gated.flags and write output/review.csv - gate_payouts flags every cash+stock terms-gate drop (no_acq_ticker, no_acq_price, no_last_close, fail_sanity) as terms_gate_failed:<reason>, appended to any flags the key already carries. csv_override stays silent (it's an intentional override, not a failure). - reconcile no longer records payout_gate_failed:* when an election settles by either leg and the regex value is within 1% of the LLM's cash leg -- the regex read a real term of the deal, just not the leg the price reconciled with. A regex value matching neither leg still keeps the flag. - classify_universe.py passes payout_flags=gated.flags to build_dlret_table and writes output/review.csv (ticker, observed_delist_date, bucket, dlret, review_flags, reason, cik, anchor_8k) for every flagged row, joining table rows to all_records on (ticker.upper(), observed_delist_date); always written (header-only when nothing is flagged) so a stale file never survives. Flag counts print in descending-count, then-name order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…never save an answer reached through a transient error The unversioned cache/ticker_resolution.json returned answers saved before the date and name checks (CPWR, SPWR, HYH, PEAK). A file without __version__ 2 is now ignored (one warning) and replaced on the next save; each entry records the member name it was checked with, and a different member name is a miss. A RequestException or 5xx in any check during one resolve() keeps that answer out of the file, like a miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…flag a missing text Credit-agreement boilerplate in takeover 8-Ks (KCI, VRTV, McDermott 2018) says "bankruptcy", so reading the whole filing would confirm a mis-tagged 1.03 and give a takeover -90%. A tag is now confirmed only when item_text(text, "1.03") finds the section and it mentions a bankruptcy. An empty text (fetch miss) still counts as confirmed and adds bankruptcy_text_missing. The rule confirms all 13 real 1.03 8-K texts in the golden fixtures and rejects VSTO. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
_AMT's lookahead (?!\.?\d) let the regex backtrack to "$1" before ",618", so "cash consideration of $1,618.7928" read 1.0 and "$12,345.678" read 12.0. The lookahead is now (?![.,]?\d). _AMT_LONG needs its "." and already reads the whole figure. No read changes over the 1,080 cached filing texts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
EdgarClient served cached submissions with no freshness check, so an event after the fetch (LBRDA's 2026-08-20 Form 25 vs a 2026-05-26 cache) was invisible. submissions(cik, fresh_after) now refetches, and rewrites the cache, when the copy was fetched before fresh_after. Each JSON payload records __fetched__; an unstamped file is dated by its mtime. classify_ticker asks once, up front, with min(observed + 45 days, today), so its later reads hit the fresh copy. A 403/429 still raises before any cache write. The test fakes accept and ignore the parameter, so the offline golden replay is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
The CIK assertion skipped the member_name_mismatch and unknown cases (IMCL, AABA, FST, BWC, HMA, LEAP, HLTH), so a wrong company there went unnoticed. All 31 cases now assert rec.cik == case.cik, and all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…ackscan; a SPAC reaching the default is an expiration; skip NYSE LLC and Cboe BZX
- Backscan: the 2.04+3.01 branch sat after the bare-3.01 branch and never ran;
a change in control (5.01 with 3.01 or 3.03, no 2.01) was not scored. The
order is now 2.01+3.01+5.01 > 2.01+5.01 > 2.01+3.01 > control > 2.04+3.01 >
3.01.
- A SPAC with no Form 25 in the window and no Form 15 reached the default
helper, where NYSE's liquidation notice ("commence proceedings to delist")
read as a deficiency (570, -100% on a ~$10 trust). The helper now checks
is_spac first: 600 expiration, flag spac.
- EXCHANGE_CIKS gains 876661 (New York Stock Exchange LLC) and 1417835 (Cboe
BZX Exchange, Inc.), as EFTS names them. 1067442 kept its NYSE label, but no
cached submission or fixture names it; it is now marked unverified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…rows in the gate summary - reconcile adds llm_gate_failed when it was given cash or election terms and nothing reconciled, keeping the existing flags. Before, a merger row whose LLM terms failed with no regex value landed at par with no flag. Stock-ratio terms go to the cash+stock gate, which flags its own drops. - GatedPayouts.gate_failed, and so the "Payout gate" summary line, counts only rows flagged payout_gate_failed that have no gated payout and no merged terms. TWO (regex $25 failed, LLM $12 settled) is no longer counted. The printed text says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…when complete; drop shadowed overrides - delist_classifications.csv was opened for writing before the loop, so an abort (EdgarBlocked, Ctrl-C) left a partial file over the committed one. It, payouts.csv, dlret.csv and review.csv are now written to .<name>.tmp in the same directory and moved into place with os.replace once complete. On an exception the temp file is removed and the old file stays. The CLI and the exit code 2 on EdgarBlocked are unchanged. - MANUAL_OVERRIDES listed VNTV and RICE twice. The earlier entries (1467373, Accenture plc; 1604665, Westlake Chemical Partners) were shadowed; they are deleted, so the dict is unchanged. The unused json import is dropped as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…o Quicksilver Resources Nine of the twelve EXCHANGE_CIKS entries were not exchanges, per EDGAR on 2026-09-17: Prudential Investment Portfolios 16, Merrill Lynch Internet Strategies Fund, a CIK with no record, Dashe Murray H, Muhlenkamp & Co, Perfect Pear, C VIII Real Estate Opportunities, T-Mobile US and Westergaard.com. The resolver skipped all nine as exchanges. The set is now the twelve verified exchange registrants: Nasdaq, NYSE, NYSE Arca, NYSE American, NYSE Texas, NYSE National, Cboe BZX, Cboe, Cboe EDGX, Nasdaq PHLX, Nasdaq Texas and the Boston Stock Exchange. MANUAL_OVERRIDES["KWK"] pointed at 1283699 (T-Mobile US), so production labelled KWK exchange_transfer 0% from T-Mobile's filings. It now points at 1060990, QUICKSILVER RESOURCES INC in the cached submissions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…r check F4 refreshed only the classifier's reads, so the resolver's date and name checks ran first on a stale copy. A cache from 2026-05-26 holds LBRDA's 2015 Form 25 but not its 2026-08-20 one, so _validate_cik rejected the right company. _fits_date, _validate_cik, _accept_member_candidate and _name_match_score now read submissions with fresh_after = edgar.submissions_fresh_after(observed), which is min(observed + 45 days, today); every recent_filings read comes after such a read. The classifier uses the same helper. The F4 classifier test pins the resolver with a manual override, so it still counts only the classifier's up-front call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…aracters
A closing 8-K disposes of warrants, preferred stock or redeemed notes in the
clause before the common share's consideration, so the flat lookback threw the
real payout away ("each Company Warrant was cancelled, and each Share converted
into the right to receive $113.00 in cash" read as {}).
_collect now scans _CLASS_CONTEXT only from the last subject marker ("each",
"per share", "holders of") inside the same 120-character lookback, so another
security's clause no longer counts as this amount's subject while "each share of
Series A Preferred Stock ... $25.00" is still discarded. The window only ever
narrows, so no new match can appear.
Also flag merger_at_par in reconstruction.enrich when a MERGER row resolves to
ASSUMED_PAR: a payout that is never found used to land at par unseen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…gged stale Every event newer than SUBMISSIONS_FRESH_DAYS refetches on every run, so one transport failure turned a ticker with a usable cached copy into an error row. _get_json now catches requests.RequestException around the refetch and returns the cached payload with __stale__ added to the returned dict only — never written to disk. EdgarBlocked still propagates (it is not a RequestException), and with no cached copy the error still propagates. classify_ticker flags submissions_stale when the submissions payload carries the mark, so the row is reviewable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…e heading item_text anchored on the first "Item N.NN" match — usually the cover-page index — and took 1,500 characters without stopping at the next item, so the standard heading "Item 1.03 Bankruptcy or Receivership" alone satisfied _confirms_bankruptcy and a mis-tagged takeover 8-K read as a bankruptcy. item_text now returns the section from the chosen match to the next Item N.NN or `width` characters, whichever comes first, and picks the first match whose section is at least 200 characters (falling back to the old behaviour when every match is shorter). _confirms_bankruptcy drops the heading line before matching mentions_bankruptcy, or takes chapter 7/11, petition, bankruptcy court or a court-appointed receiver anywhere in the section. Corpus (1,236 cached + fixture texts): all 13 real Item 1.03 8-Ks still confirm and VSTO still does not; over the 408 filings carrying an Item 3.01, neither cites_listing_deficiency (31) nor says_listing_transfer (4) changes on any row. The `receivers?` term is beyond the reviewer's list: without it HLTH/Nobilis, a state-court receivership whose body says only "Temporary Receiver", stopped confirming and its golden case flipped from liquidation to compliance_failure, reversing the Task 1 concern-3 ruling. `receivers?` does not match the heading's "Receivership", so the heading still confirms nothing on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…e read _default_without_fingerprint fell through to the merger-evidence branch — the least conservative option — on the path where the evidence is weakest. When notice_text_missing is set, the branch now needs a proxy or tender filing within 120 days of the anchor instead of 400; otherwise the row continues to the NT 10-K/Q branch (580) or to unknown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
… the merger bankruptcy_8ks(..., before=540) plus the confirmed-bankruptcy override beat every merger signal, so a company that filed Chapter 11, emerged, and was acquired within 18 months was marked liquidation (-90% in training). The merger path now wins when every confirmed item 1.03 is more than 180 days before the delisting AND an 8-K within +/-30 days of it carries a change in control (5.01, or 2.01 with 3.01 or 3.03), and the record is flagged bankruptcy_before_merger. A confirmed 1.03 inside 180 days, a second bankruptcy near the delisting, or a bare 2.01 all keep the override. _confirmed_bankruptcy takes the window as an argument so the "no recent bankruptcy" question is asked directly, and the change-in-control fingerprint becomes one helper shared with _rename_or_transfer instead of a fourth copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
member_name_mismatch was computed for manual overrides too, so the ~40 hand-verified pins (the new KWK pin among them) showed up in review.csv. The member name is a check on the automatic resolver; a pin has already been checked by hand, so the check and the flag are skipped when resolution.source is "manual". Golden: IMCL is pinned through GOLDEN_MANUAL, so its expected_flags loses member_name_mismatch. The row's note records why. Production resolves IMCL the same way (MANUAL_OVERRIDES), so its dlret row loses the flag too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…he capture date.fromisoformat(f.filing_date) ran without the empty-string guard _trim has two lines earlier, so one filing with an empty filingDate aborted a capture after the network work was already spent. The window filter moves into _near_event, which skips empty dates the way _trim does, and is covered by a unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
_effective_items ran twice on the same 8-K in the Form 25 path — once to test for None, once to keep the result — refetching the text, re-running the 1.03 confirmation and making the flag side effects order-dependent. It is now computed into a local and recomputed only when the backscan replaces the 8-K. Also record above EXCHANGE_CIKS that the set was checked against the 25-NSE filer list (no IEX, Cboe EDGA, MEMX, MIAX Pearl or LTSE filer exists). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…ing 400 The R4 branch chose between 120 days and a literal 400 that had to stay in sync with merger_evidence's default. MERGER_EVIDENCE_DAYS is now the one source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…s a pin Reverses R6. member_name_mismatch states a fact about the security — the vendor series is not the named member — not a to-do about the CIK, and it is the signal the consumer uses to catch an impostor series (IMCL is ImmunoClin trading under ImClone's old symbol). A manual pin must not silence it, so the check runs for every resolution again and IMCL's golden row is restored unchanged. A manual resolution now also carries resolved_by_manual_override, so review triage can tell "the name differs" apart from "the CIK needs checking". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…rees resolved_by_manual_override was unconditional, so all 46 pinned tickers landed in review.csv — a superset of the volume the original R6 finding complained about. review.csv lists rows the rules could not settle, and a pin whose name agrees is settled, so the flag now only qualifies member_name_mismatch: it is appended inside the mismatch branch, after the name check has run. A pin whose name differs still carries both flags; a pin whose name agrees carries neither. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
Drops the local environment details from the branch: the conda env name, the consumer repo's name and paths, and the price-snapshot directory. The golden fixture no longer records the snapshot path it was built from; the acquirer price it stores is the provenance that matters, and data/golden_events.csv already names it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
…3 caption
Two Important defects from the re-review, both in R3's new code.
_BANKRUPTCY_BODY's `petition` was unanchored and matched inside "competition".
Takeover 8-Ks routinely report antitrust and competition clearance, so a
mis-tagged 1.03 confirmed as a bankruptcy and the row became liquidation at
-0.90. It is now `\bpetitions?\b`.
_drop_heading only removed a heading that ended in punctuation, and its newline
branch is dead in production: edgar._strip_html collapses every newline to a
space (0 of 1,080 cached texts contain one), so an unpunctuated heading kept its
own words and the caption alone still confirmed — the thing R3 exists to prevent.
6 of the 13 real Item 1.03 filings kept heading words, SVB among them
("Item 1.03. Bankruptcy or Receivership On March 10, 2023, ..."). _drop_heading
now matches SEC's caption itself and takes it off, falling back to the
newline/sentence rules only for a heading it does not recognise. 0 of 13 keep it
now, and SVB still confirms on the body's own "appointed as receiver".
Corpus unchanged otherwise: 13 of 13 real Item 1.03 filings confirm, VSTO still
does not, and over the 408 filings carrying an Item 3.01 cites_listing_deficiency
(31) and says_listing_transfer (4) are the same on every row.
Also pin the stale fallback's 5xx behaviour with tests and say in the _get_json
docstring that it is deliberate: requests.HTTPError is a RequestException, so an
SEC outage serves the cached copy instead of erroring the row out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz
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.
Makes the classifier get renames, recycled tickers, bankruptcies, payouts and impostor series right the first time, so a downstream consumer no longer needs hand-written corrections on top of
dlret.csv.A consumer typically turns a
compliance_failurerow into a −100% return and aliquidationrow into −90%, so a wrong bucket fabricates a large loss and a wrong payout fabricates a wrong return. Every rule below exists to keep that from happening without positive evidence.What changed
Golden harness (Task 1).
data/golden_events.csvholds 31 verified events;tests/fixtures/golden/holds the recorded EDGAR, EFTS and LLM responses;tests/golden.pyreplays them offline.scripts/build_golden_fixtures.pyrebuilds them.SEC blocks are loud (Task 2). A 403 or 429 raises
EdgarBlockedinstead of being cached as "no CIK found". Misses are no longer persisted, and oldcik: nullentries are ignored. The CLI exits 2.Identity (Task 3). The resolver takes index-member names, compares them near the event date, and keeps a disagreeing EFTS answer as a flagged fallback rather than letting a live company with the same name replace the company that filed the delisting forms. Rows carry
member_name_mismatchandresolved_by_current_ticker_map.Classifier rules (Tasks 4–9).
frozen_tail:<days>.Payouts (Tasks 10–11). The reader handles whole-dollar amounts, ignores preferred redemptions and award tables, treats elections as mixed, and abstains on ambiguous ties.
payout_gate.reconcilethen checks every payout against the last trade close: one that does not reconcile is replaced by LLM terms that do, or dropped. An election takes the leg the close matches.payouts.csvcarries only checked values.Review surface (Task 12).
dlret.csvgains a trailingreview_flagscolumn, andoutput/review.csvlists every row the rules could not settle, with its cik and anchor 8-K.Final-review fixes. The resolver cache is versioned, so a cache holding old wrong CIKs is ignored. A cached SEC filing list saved before the event is refetched, and a failed refetch serves the cached copy flagged
submissions_staleinstead of failing the row.EXCHANGE_CIKSnow holds 12 exchange registrants verified against EDGAR and against the Form 25-NSE filer list: 9 of the previous 12 entries were unrelated filers.MANUAL_OVERRIDES["KWK"]pointed at the wrong company and now points at Quicksilver Resources (1060990). Outputs are written to a temp file and replaced only when complete.Code-review round. A later review found and this branch fixes: the payout class guard discarding the real common-share consideration when the preceding clause mentioned warrants, preferred stock or redeemed notes;
item_textreading the cover-page index instead of the item's own section; the standard "Bankruptcy or Receivership" caption confirming a bankruptcy by itself; an unreadable 3.01 notice falling through to the merger branch; a bankruptcy the company emerged from outranking a later merger; and a fixture-builder crash on a filing with an empty date. New review flags:merger_at_par,submissions_stale,bankruptcy_before_merger.Testing
413 tests pass, all offline. The 3 warnings predate this branch. All 31 golden cases pass on bucket, code, resolved CIK, flags and payout. Corpus checks over the cached filings: all 13 real Item 1.03 filings still confirm as bankruptcies and a mis-tagged one does not; deficiency and listing-transfer detection are unchanged on all 408 Item 3.01 filings.
Behaviour changes worth knowing
Follow-ups
An acceptance run against a full universe, plus README, CLAUDE.md and
docs/data-flow.mdupdates, is still open (Task 13 of the plan). The plan file's "Execution record" section lists every ruling made during execution and the carry-over items.🤖 Generated with Claude Code
https://claude.ai/code/session_01UauyzPDg1987fjTMJKrTkz