Stop the deterministic passes from silently truncating the sections they preempt - #318
Conversation
SKIP_PURPOSE matched its total/source phrases ANYWHERE in a row label, but
those phrases are also how filers qualify a real line item. The
underwriting-commission row is routinely written "Underwriting commissions
(2.0% of gross proceeds from units offered to public)" and the residual trust
row "Not held in trust account after offering expenses", so both were dropped.
That is silent deletion, not a missed hit: `use-of-proceeds` declares
`clears: {use_of_proceeds}` and the deterministic pass declares the identical
bare `covers`, so it preempts the model on every SPAC filing, and the table has
already been emptied by the time the parse runs. The section then resolves
clean, with no dead letter and the same result on every replay. Measured
against the committed golden labels: 13 filings, 16 line items — the largest
expense row in each — and the same code path runs under extractor id `424` for
the priced prospectus, i.e. the final deal figures.
Every alternative is now anchored, or scoped to where it means what it says:
- the total/source/ratio family is anchored at the start of the label, which
is where a filer names a row that is the table's own arithmetic;
- `reimbursed expenses` is dropped entirely — the golden labels record it as a
real line item, and the parse now emits it (the label set for one Churchill
fixture was missing the row its identically-tabled sibling carries, added
here from the filing);
- the per-share/per-unit metric rules are tested against the label with its
parentheticals removed, so a metric row is still skipped while a line item
qualified "($10.20 per unit)" survives.
Anchoring exposed the other half of the shape: a filer who factors the sources
into a block under a bare "Gross proceeds" heading writes its children as plain
labels ("Offering", "Private Units"), which only the heading identifies as
sources. Such a heading now opens a block that the matching expenses heading
closes.
`useOfProceedsIsComplete` reports whether the walk enumerated the table, from
its own decline log rather than a second reading: a labelled row between the
first and last line item, carrying no readable figure and matching no declared
rule, is a row the parse could not represent. It errs toward incomplete, which
costs a model call rather than a filed line item.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ske1Jwk7fDFxHykfZGEzce
`preempts()` authorized a model-free parse to replace the model whenever `covers` was a superset of `clears`, and both sets name DESTINATIONS. For a destination holding many rows that says only "the parse fills every column persist writes" — never "the parse found every ROW". The caller has already cleared the destination, so a walk that reads N of M rows persists N and the section is marked resolved: no dead letter, no diagnostic, the same answer on every replay. The unanchored use-of-proceeds skip rules fixed in the previous commit were one instance of exactly this hole, and two more were live: - `spac-sponsors` covers `clears` exactly, so `parseSpacSponsors` preempted on its two prose patterns. Both require `our|the` immediately before `sponsor`, so a vehicle whose second sponsor is introduced as "our co-sponsor, Beta Holdings LLC, is …" rebuilt an already-cleared `spac_sponsor_link` with one of two sponsors. - `beneficial_ownership` under a full `ownershipCoverage`: rows failing `looksLikeOwner` are dropped and ones whose stub carries a street number are truncated by `peelName`, before persist and before the section resolves. `DeterministicPass.complete` already expressed the missing claim but was read only for roster closure, and no wired pass declared it. It is now the row half of the contract and a precondition of preempting at all, checked through `assertsCompletePopulation` (a missing or throwing claim declines, matching how a throwing `covers` is treated). A pass that cannot say its rows are the whole population costs a model call instead of losing filed rows. Declared per pass, from what each destination and walk can honestly support: - `spac-classification` and `sponsor-promote` write one row per filing, so producing that row IS enumerating the population — they keep preempting, and `promoteCoverage` keeps answering the column question; - `use-of-proceeds` claims completeness from the walk's own decline log (`useOfProceedsIsComplete`), which holds on 16 of the 20 committed SPAC fixtures it parses and correctly declines the one whose trust row carries no figure; - `spac-sponsors` and `beneficial-ownership` declare nothing and stop preempting: neither prose regexes nor a table walk that filters its own rows can report that the section named no one else. The corpus tests gain the recall side, which every existing assertion missed because a dropped row invents nothing: sponsors and classification must agree with the golden labels on every filing they answer for, the promote pass must be right about every column its coverage claims (80 field checks over 17 filings), and the ownership walk's four known drops are pinned as a list so a new one fails and closing one prompts revisiting the claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ske1Jwk7fDFxHykfZGEzce
Both pinned the preemption rule the previous commit replaced, and both were mirror images of what the rule now says. `preempts()` used to authorize a model-free parse on column coverage alone, so `spac-sponsors` and `beneficial-ownership` stood in for the model; requiring a row-completeness claim retires both, because two prose patterns cannot report that the section named no other sponsor and a walk that filters its own rows cannot tell a row it dropped from a row the table never had. The destination is cleared before persist, so a subset there is filed data lost with no dead letter — the model call is the cheaper side of that trade. So the assertions flip rather than relax. Each test now proves the section reaches the model and that the entity is still persisted through it: the section's prompt appears in the recorded calls, and the observation's provenance carries the model's id instead of `deterministic`. Each also opens by running the deterministic parse over the same section text and requiring it to read the filing outright, which is what makes the filing the case that demonstrates the rule — coverage is not the question, and a parse that handles the section perfectly still does not get to answer for it. The ownership resale test keeps every assertion and gains an accurate name: it no longer discriminates preemption from non-preemption, since no ownership section preempts now, and what it verifies is that the class, offered and after-offering figures the table states survive the model path — the three the walk would have written null. Verified on this branch: `bun run test` 428 files / 3885 tests passed, 3 files / 20 tests skipped, 0 failed; `bun run format-check` and `bun run build` clean. The two tests were confirmed to pass on `code-extractors` and fail at this branch's head before the change, and the replacements fail on `code-extractors` and pass here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ske1Jwk7fDFxHykfZGEzce
|
The two red tests encoded the preemption contract this PR supersedes: both asserted that They now assert the mirror image: the section's prompt reaches the model, and the persisted observation's provenance carries the model id rather than Verified on Generated by Claude Code |
Two HIGH-severity correctness bugs on this branch, one commit each. Both are the same shape: a model-free parse replaces the model on a section whose destination the caller has already cleared, so a row the parse never produced is a row the filing loses — with no dead letter, no diagnostic, and the same result on every replay.
1.
SKIP_PURPOSEmatched its total/source phrases anywhere in the labelparseSpacUseOfProceedsdropped any row whose label containedgross proceedsoroffering expenses, which is how most filers qualify the two rows that matter most:Underwriting commissions (2.0% of gross proceeds from units offered to public, excluding deferred portion)— the largest expense in the tableNot held in trust account after offering expenses— the residual trust rowMeasured against
src/eval/goldenS1Labels.ts: 13 filings, 16 line items lost. The same code path runs under extractor id424for the priced 424B1/424B4, i.e. the final deal figures. Example — CIK 1848507, accession 0001193125-21-066104, losesUnderwriting commissions (2% of gross proceeds …) = $7,000,000andNot held in trust account after offering expenses = $525,000.Every alternative was reviewed, not only the two named:
gross proceeds,proceeds after,% public offering,offering expenses^proceeds from,^from,^total, the financial-statement captionsreimbursed expensesper share/per unitHeld in trust account ($10.20 per unit)survives while a dilution-table metric row does notTwo follow-on findings the anchoring surfaced:
s1_2134856) previously extracted nothing at all — the underwriting-discount row was the onlySPAC_USEanchor and it was being dropped, so the whole table failed its own presence check. It now yields 18 rows.Gross proceedsheading writes the children as plain labels (Offering,Private Units). Such a heading now opens a source block that the matchingOffering expensesheading closes.One golden label was wrong rather than the parser:
s1_2114227was missing theReimbursed expensesrow its identically-tabled siblings1_2114229carries. Added from the filing (| Reimbursed expenses(4) | | 3,000,000 | | | 3,675,000 |).2.
preempts()authorized a parse on column coverage alonecoversandclearsboth name destinations. For a multi-row destination a bare table name asserts only "the parse fills every columnpersistwrites" — never "the parse found every ROW".DeterministicPass.completealready expressed the missing claim but was read only fors1:managementroster closure, and no wired pass declared it.Two further live instances beyond finding 1:
spac-sponsors—coversequalsclearsexactly. Both prose patterns requireour|theimmediately beforesponsor, soour co-sponsor, Beta Holdings LLC, is …matches neither and an already-clearedspac_sponsor_linkwas rebuilt with one of two sponsors.beneficial_ownershipunder a fullownershipCoverage— rows failinglooksLikeOwnerare dropped and stubs carrying a street number are truncated bypeelName, before persist and before the section resolves.completeis now the row half of the contract and a precondition of preempting, checked through a newassertsCompletePopulation(a missing or throwing claim declines, matching how a throwingcoversis already treated).What each pass can honestly claim, and what that costs
spac-classificationsponsor-promote(extractor, accession);promoteCoveragestill answers the column questionuse-of-proceedsspac-sponsorsbeneficial-ownershipCost/behaviour trade-off. On the committed 42-filing corpus this adds model calls for 7 sponsor sections and 4 use-of-proceeds sections, and removes none elsewhere;
beneficial-ownershipandsponsor-promotenever reached full coverage anyway, so nothing changes for them today. The sponsor and ownership sections are the ones where the honest answer is "this parser cannot assert completeness and must stop preempting" — losing the saving beats losing filed rows.use-of-proceedskeeps most of its saving because its decline log is a genuine claim derived from the same walk, not a second reading. It errs toward incomplete: three fixtures are declined over sub-table headings that read identically to a blank-figure line item, which costs a model call. The one it declines correctly iss1_2147219, whoseHeld in trust accountrow states its amount only as100.0% of public offering size— golden records it withamount: null, and the parse cannot represent it.Tests
The recall assertions are the fix's safety net — every pre-existing corpus assertion is precision-only (
if (parsed.length === 0) continueplus "does not invent names/purposes outside the golden set"), so a dropped row could never fail CI.parseSpacUseOfProceeds.corpus.test.ts— must find every golden line item on a filing it claims to have enumerated (reverting the anchoring makes this red with all 16 lost rows named), plus a floor on how often the completeness claim holds so the predicate cannot degrade into "always false".parseSpacSponsors.corpus.test.ts— must find every golden sponsor on a filing it hits; the co-sponsor blind spot is pinned directly, so a straycomplete: () => truecannot land as a tidy-up.parseSpacClassification.corpus.test.ts— must agree with the golden verdict on every filing it answers. It is the one wired parse whose result is never checked against a model.parseOfferingTables.corpus.test.ts— the promote pass must be right about every columnpromoteCoverageclaims, with no null forgiveness (80 field checks across 17 filings).parseBeneficialOwnership.corpus.test.ts— the walk's four known drops (AIGH;Acuitas Group Holdings, LLC,Acuitas Capital LLC,Dorado Goose, LLC) are pinned as an exact list. A new gap fails; closing one also fails, which is the prompt to reconsider the claim. Reported rather than fixed here: the pass no longer preempts, so nothing is lost by them today.deterministicPass.test.ts/sectionRunner.deterministic.test.ts— full column coverage alone no longer preempts; a false or throwing completeness claim declines.Two storage tests re-pointed at the new contract (third commit)
Form_S_1.storage.sponsors.test.tsandForm_S_1.storage.ownership.test.tspinned the superseded preemption contract — the sponsors test asserted the section resolved "without calling the sponsor model", which is exactly what finding 2 removes. Both were verified to pass on base and fail on this branch, i.e. they are this PR's intended behaviour change and not an unrelated break.They now assert the mirror image: the section reaches the model, the entity is still persisted through that path, and provenance carries the model id and explicitly not
DETERMINISTIC_MODEL_ID. Neither was weakened — each now opens by running the deterministic parse over the same section text and requiring it to read the filing outright, so the filing is demonstrably the preemptable-looking case and the point is that it reaches the model anyway. Applied to the base branch, both new files fail there and pass here. No assertion was deleted.Verification
CI
buildgreen onbbb7837.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ske1Jwk7fDFxHykfZGEzce