Skip to content

Commit bbb7837

Browse files
committed
Re-express the two storage tests against the row-completeness contract
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
1 parent fa62f14 commit bbb7837

2 files changed

Lines changed: 118 additions & 12 deletions

File tree

src/sec/forms/registration-statements/Form_S_1.storage.ownership.test.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import { resetDependencyInjectionsForTesting } from "../../../config/TestingDI";
99
import { setupAllDatabases } from "../../../config/setupAllDatabases";
1010
import { BeneficialOwnershipRepo } from "../../../storage/beneficial-ownership/BeneficialOwnershipRepo";
1111
import { CompanyObservationRepo } from "../../../storage/observation/CompanyObservationRepo";
12+
import { ObservationProvenanceRepo } from "../../../storage/provenance/ObservationProvenanceRepo";
13+
import { parseEdgarHtml } from "../../html/parseEdgarHtml";
1214
import { processFormS1 } from "./Form_S_1.storage";
15+
import { S1_SECTIONS } from "./s1/DocumentSegmenter";
16+
import { DocumentTreeSegmenter } from "./s1/DocumentTreeSegmenter";
17+
import { parseBeneficialOwnership } from "./s1/parseBeneficialOwnership";
18+
import { DETERMINISTIC_MODEL_ID } from "./s1/parseOfferingTables";
19+
import { resolveModelId } from "./s1/s1Model";
1320
import { fakeS1Model, registerFakeStructuredProvider } from "./s1/testing/fakeStructuredProvider";
1421

1522
const HTML_PARSEABLE = [
@@ -56,6 +63,44 @@ const MANAGEMENT_PAYLOAD = {
5663
],
5764
};
5865

66+
const SPAC_OWNERS_PAYLOAD = {
67+
owners: [
68+
{
69+
name: "Halyard Sponsor III LLC",
70+
owner_kind: "company",
71+
security_class: null,
72+
shares_owned: 4312500,
73+
percent_owned: 100,
74+
shares_offered: null,
75+
shares_after: null,
76+
percent_after: null,
77+
is_selling_stockholder: false,
78+
footnote: null,
79+
confidence: 0.9,
80+
source_span: "Halyard Sponsor III LLC",
81+
},
82+
{
83+
name: "Eleanor Vasquez",
84+
owner_kind: "person",
85+
security_class: null,
86+
shares_owned: 4312500,
87+
percent_owned: 100,
88+
shares_offered: null,
89+
shares_after: null,
90+
percent_after: null,
91+
is_selling_stockholder: false,
92+
footnote: null,
93+
confidence: 0.9,
94+
source_span: "Eleanor Vasquez",
95+
},
96+
],
97+
};
98+
99+
function ownershipSectionText(html: string): string {
100+
const segmented = new DocumentTreeSegmenter().segment(parseEdgarHtml(html, "s1.htm"));
101+
return segmented.find((s) => s.name === S1_SECTIONS.BENEFICIAL_OWNERSHIP)?.text ?? "";
102+
}
103+
59104
let cleanup: (() => void) | undefined;
60105

61106
describe("processFormS1 beneficial ownership", () => {
@@ -69,8 +114,30 @@ describe("processFormS1 beneficial ownership", () => {
69114
resetDependencyInjectionsForTesting();
70115
});
71116

72-
it("persists a SPAC ownership table with no offered/after columns as deterministic", async () => {
73-
const { unregister } = registerFakeStructuredProvider([MANAGEMENT_PAYLOAD]);
117+
// The table walk covers every column this table prints — a SPAC's pre-IPO
118+
// table states one class and no offered/after position, so the six columns
119+
// the parse hardcodes null are the disclosure — and it still does not stand
120+
// in for the model. `ownershipCoverage` answers the COLUMN question only; the
121+
// walk drops a stub failing `looksLikeOwner` and truncates one carrying a
122+
// street number, so it cannot report that these two are the whole roster, and
123+
// `beneficial_ownership` is cleared before persist. This filing is the case
124+
// that makes the rule visible: the parse reads the table outright and the
125+
// model runs anyway.
126+
it("sends a fully covered SPAC ownership table to the model, because the walk cannot claim it read every row", async () => {
127+
expect(
128+
parseBeneficialOwnership(ownershipSectionText(HTML_PARSEABLE)).map((r) => [
129+
r.owner_kind,
130+
r.shares_owned,
131+
])
132+
).toEqual([
133+
["company", 4312500],
134+
["person", 4312500],
135+
]);
136+
137+
const { calls, unregister } = registerFakeStructuredProvider([
138+
MANAGEMENT_PAYLOAD,
139+
SPAC_OWNERS_PAYLOAD,
140+
]);
74141
cleanup = unregister;
75142

76143
await processFormS1({
@@ -89,23 +156,31 @@ describe("processFormS1 beneficial ownership", () => {
89156
model: fakeS1Model(),
90157
});
91158

159+
expect(calls.some((p) => /Extract every beneficial owner/.test(p))).toBe(true);
160+
92161
const rows = await new BeneficialOwnershipRepo().queryByAccession("acc-own-1");
93162
expect(rows.map((r) => [r.owner_kind, r.shares_owned])).toEqual([
94163
["company", 4312500],
95164
["person", 4312500],
96165
]);
97166
const companies = await new CompanyObservationRepo().listAll();
98167
expect(companies.some((c) => /Halyard Sponsor/i.test(c.name ?? ""))).toBe(true);
99-
// The table prints one class, no offered/after columns and no selling
100-
// stockholders, so the six columns the parse hardcodes null are what this
101-
// filing actually discloses — nothing is lost by writing them.
102168
expect(rows[0]!.security_class).toBeNull();
103169
expect(rows[0]!.shares_after).toBeNull();
104170
expect(rows[0]!.is_selling_stockholder).toBe(false);
171+
// The rows are the model's, not the walk's, and provenance says so.
172+
const provenance = await new ObservationProvenanceRepo().get(
173+
"company",
174+
rows[0]!.observation_id!
175+
);
176+
expect(provenance?.model_id).not.toBe(DETERMINISTIC_MODEL_ID);
177+
expect(provenance?.model_id).toBe(resolveModelId(fakeS1Model()));
105178
});
106179

107-
it("does not preempt the ownership model on a resale table with Shares Offered / Shares After columns", async () => {
108-
// The same parse, the same table shape, the opposite verdict: here the
180+
it("persists the class / offered / after figures a resale table states, which the walk would have written null", async () => {
181+
// The row half above already sends every ownership section to the model, so
182+
// this filing gets there for a second, independent reason: `ownershipCoverage`
183+
// declines the column half outright, and the walk never even runs. Here the
109184
// filing DOES state a class, an offered count and an after-offering
110185
// position, so the parse's hardcoded nulls would delete three disclosed
111186
// figures — and `is_selling_stockholder: false` would assert this holder

src/sec/forms/registration-statements/Form_S_1.storage.sponsors.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ import { resetDependencyInjectionsForTesting } from "../../../config/TestingDI";
99
import { setupAllDatabases } from "../../../config/setupAllDatabases";
1010
import { CompanyObservationRepo } from "../../../storage/observation/CompanyObservationRepo";
1111
import { ObservationProvenanceRepo } from "../../../storage/provenance/ObservationProvenanceRepo";
12+
import { parseEdgarHtml } from "../../html/parseEdgarHtml";
1213
import { processFormS1 } from "./Form_S_1.storage";
14+
import { S1_SECTIONS } from "./s1/DocumentSegmenter";
15+
import { DocumentTreeSegmenter } from "./s1/DocumentTreeSegmenter";
1316
import { DETERMINISTIC_MODEL_ID } from "./s1/parseOfferingTables";
17+
import { parseSpacSponsors } from "./s1/parseSpacSponsors";
18+
import { resolveModelId } from "./s1/s1Model";
1419
import { fakeS1Model, registerFakeStructuredProvider } from "./s1/testing/fakeStructuredProvider";
1520

21+
const SPONSOR_SENTENCE =
22+
"Our sponsor, Acme Sponsor LLC, is a Delaware limited liability company and was formed to invest in us.";
23+
1624
const HTML_PARSEABLE = [
1725
"<h1>MANAGEMENT</h1><p>x</p>",
1826
"<h1>THE SPONSOR</h1>",
19-
"<p>Our sponsor, Acme Sponsor LLC, is a Delaware limited liability company and was formed to invest in us.</p>",
27+
`<p>${SPONSOR_SENTENCE}</p>`,
2028
"<h1>LEGAL MATTERS</h1><p>x</p>",
2129
].join("");
2230

@@ -28,6 +36,13 @@ const HEADER_6770 = {
2836
filingDate: null,
2937
};
3038

39+
const SPONSOR_SPAN = "Our sponsor, Acme Sponsor LLC, is a Delaware limited liability company";
40+
41+
function sponsorSectionText(html: string): string {
42+
const segmented = new DocumentTreeSegmenter().segment(parseEdgarHtml(html, "s1.htm"));
43+
return segmented.find((s) => s.name === S1_SECTIONS.THE_SPONSOR)?.text ?? "";
44+
}
45+
3146
let cleanup: (() => void) | undefined;
3247

3348
describe("processFormS1 spac-sponsors", () => {
@@ -41,7 +56,18 @@ describe("processFormS1 spac-sponsors", () => {
4156
resetDependencyInjectionsForTesting();
4257
});
4358

44-
it("persists a parseable sponsor sentence as deterministic without calling the sponsor model", async () => {
59+
// The sponsor pass is two prose patterns, both requiring `our|the` immediately
60+
// before `sponsor`. Reading THIS sentence says nothing about whether the
61+
// section introduced a second sponsor some other way ("our co-sponsor, Beta
62+
// Holdings LLC, is …"), and `spac_sponsor_link` is cleared before persist — so
63+
// the pass declares no completeness and never stands in for the model, however
64+
// cleanly it reads the prose. This filing is the case that makes the rule
65+
// visible: the parse handles the sentence outright and the model runs anyway.
66+
it("sends a parseable sponsor sentence to the model, because the prose parse cannot claim the section named no one else", async () => {
67+
expect(parseSpacSponsors(sponsorSectionText(HTML_PARSEABLE)).map((r) => r.legal_name)).toEqual([
68+
"Acme Sponsor LLC",
69+
]);
70+
4571
const { calls, unregister } = registerFakeStructuredProvider([
4672
{
4773
focus: [],
@@ -50,11 +76,14 @@ describe("processFormS1 spac-sponsors", () => {
5076
team: null,
5177
url_spac: null,
5278
confidence: 0.9,
53-
source_span: "Our sponsor, Acme Sponsor LLC, is a Delaware limited liability company",
79+
source_span: SPONSOR_SPAN,
5480
},
5581
{ people: [] },
5682
{ owners: [] },
5783
{ parties: [] },
84+
{
85+
sponsors: [{ legal_name: "Acme Sponsor LLC", confidence: 0.9, source_span: SPONSOR_SPAN }],
86+
},
5887
]);
5988
cleanup = unregister;
6089

@@ -74,13 +103,15 @@ describe("processFormS1 spac-sponsors", () => {
74103
model: fakeS1Model(),
75104
});
76105

106+
expect(calls.some((p) => /Identify each sponsor entity/.test(p))).toBe(true);
107+
77108
const companies = (await new CompanyObservationRepo().listAll()).filter((c) =>
78109
/s1:spac-sponsor/.test(c.source_context ?? "")
79110
);
80111
expect(companies.some((c) => /Acme Sponsor/i.test(c.name ?? ""))).toBe(true);
81-
expect(calls.some((p) => /Identify each sponsor entity/.test(p))).toBe(false);
82112
const party = companies.find((c) => /Acme Sponsor/i.test(c.name ?? ""));
83113
const provenance = await new ObservationProvenanceRepo().get("company", party!.observation_id);
84-
expect(provenance?.model_id).toBe(DETERMINISTIC_MODEL_ID);
114+
expect(provenance?.model_id).not.toBe(DETERMINISTIC_MODEL_ID);
115+
expect(provenance?.model_id).toBe(resolveModelId(fakeS1Model()));
85116
});
86117
});

0 commit comments

Comments
 (0)