Fix the SPAC demotion self-defeat and the fabricated parent legal form - #291
Open
sroussey wants to merge 1 commit into
Open
Fix the SPAC demotion self-defeat and the fabricated parent legal form#291sroussey wants to merge 1 commit into
sroussey wants to merge 1 commit into
Conversation
M1 — the `alreadyKnownSpac` guard was defeated by the filing's own earlier pass. FOUR pre-segmentation paths call `recordRegistration` (no model, S-1MEF, parse error, Part-II-only amendment), each of which appends an event and rebuilds the spac row, so `getSpac(cik)` was already defined on the `retry-dead-letters` run that could finally read the prospectus — and a post-de-SPAC filer under a stale 6770 header was never demoted. The guard now reads the event log and requires evidence from a DIFFERENT accession. The early `recordRegistration` calls stay: on the MEF and Part-II-only paths the header SIC is the only signal that accession will ever have. M2 — `copyForm` grafted the parent's trailing legal form onto the divisional name, fabricating a name that was never filed. On the canonicalize-identity forms (LP, LLC, Trust) it survives `normalizeCompanyName`, so "Cantor Fitzgerald Securities, a division of Cantor Fitzgerald, L.P." minted a second canonical company beside the same underwriter named plainly. `copyForm` and `trailingLegalForm` are deleted; both split branches record X exactly as filed. The parent is already carried in `familyName` and `source_context.family_name`. LOW — pickLatestTrustFact's doc said "later (or equal) filed date wins" while the code is a strict `>`. The code is right; the prose is fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UW1Qr5mxetAQr61YKEY9nz
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.
Two independent fixes in the S-1 / company-observation path, plus a doc correction.
M1 — the
alreadyKnownSpacguard is defeated by the filing's own earlier passThe header-SIC demotion never fires for a post-de-SPAC filing that had to be retried, because four pre-segmentation paths in
processFormS1callrecordRegistration(baseReg)::339:386S-1MEF/F-1MEF:409:442recordRegistrationappends aregistrationevent and rebuilds thespacrow (SpacReportWriter.ts:197), sogetSpac(cik)is already defined by the time aretry-dead-lettersrun can finally read the prospectus. The guard could not distinguish a row this same accession minted on an earlier pass, so the demotion never ran.Narrowed rather than removed:
The invariant being protected is that a CIK which once registered as a blank check stays a SPAC CIK — that requires evidence from a different accession; this filing is not evidence about itself. The
spacrow is derived from the append-only event log, so it always carries at least one event: this is a faithful narrowing ofgetSpac(cik) !== undefined, not a change of subject.The early
recordRegistrationcalls are deliberately kept. On the MEF and Part-II-only paths there is no prospectus to judge, so the header SIC is the only signal that accession will ever have; skipping the record would lose real SPACs whose only filing a sweep saw was a MEF.Residual, unavoidable: a de-SPAC filer whose first seen filing was a MEF under a stale 6770 header still mints a row and still gates its later filings. A MEF is a cover page with nothing in it to judge.
Tested with the two-run shape that is the only one reproducing it — pass 1 with no model (asserts the row is minted and
s1_classificationreadssgml-header), pass 2 on the same accession with a model (assertssgml-header-rejected) — plus a control seeding a prior event from a different accession and asserting no demotion. Confirmed the new test fails against the old guard and the control passes under both.M2 —
copyFormfabricates a legal form and mints a second canonical companysplitParentClausegrafted the parent's trailing legal form onto the divisional name. Verified:Cantor Fitzgerald Securities, a division of Cantor Fitzgerald, L.P.Cantor Fitzgerald Securities LPCantor Fitzgerald Securities LPCantor Fitzgerald Securities(filed plainly)Cantor Fitzgerald SecuritiesLP is a
canonicalizeform, notstrip, so the fabricated suffix survivesnormalizeCompanyName— two canonical companies and two identity links for one firm. Same forLLCandTrust.Corpconverged only by accident, becauseCorphappens to be stripped.copyFormandtrailingLegalFormare deleted and both split branches now recordobservationName: x. Copying only thestripforms would be a no-op on identity by construction and would still fabricate a name that was never filed, so nothing is copied at all. The parent is already recorded — infamilyNameand insource_context.family_name— and the observation's only job is to be the name as filed.Two live call sites persist
observationNameascompany_observations.name:Form_S_1.storage.ts:1209(sponsors) ands1/offeringSections.ts:456(underwriters).sec resolve --renormalizerecomputesnormalized_namefrom the storedname, and the stored name is the fabricated one — so re-normalizing changes nothing here. Historical convergence needs either re-extraction (AI cost) or a hand alias per affected pair.Blast radius is bounded:
4b7ab4blanded within the last 24h, so the alias route is almost certainly cheaper. List the affected observations first:The
instr/positionclause is the test for a fabricated form: the storednameis not a substring of the name as filed precisely when a suffix the filer never wrote was appended. Each row it returns is one alias:LOW —
pickLatestTrustFactdoc/code disagreementsrc/storage/spac/pickLatestTrustFact.ts:54-56said "later (or equal) filed date wins" while the code isincoming.filed > existing.filed. The code is right — a 10-Q/A has a strictly later filed date, and an equal-filed re-write is churn — so only the prose changed.Tests
splitParentClause.test.ts— the copy assertions inverted; the GP-copy case dropped; "does not copy a form when X already has one" kept as a now-trivially-true regression pin; new Cantor case asserting the observation name, that it normalizes equal to the plainly-filed name, and thatfamilyNamestill carriesCantor Fitzgerald, L.P.; the same forLLCandTrust;parentClauseSourceContextstill carryingas_filed+family_name.Form_S_1.storage.underwriters.test.ts— the end-to-end split test asserted the fabricatedKingswood Capital Markets Inc; now assertsKingswood Capital Markets, with the canonical family row assertions unchanged.headerSicDowngrade.test.ts— the two-run reproduction and its control described above.Note
legalFormTrailingCanonical(src/util/legalForms.ts) now has no production consumer —splitParentClausewas its only one. It is left exported and tested rather than removed: it is part of the one shared legal-form vocabulary, and deleting it here would collide with the separate prose-matcher change in #289.Generated by Claude Code