Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
80ccac2
feat(spac): derive deal attempts from the 8-K event stream
claude Jun 23, 2026
dad6d80
refactor(spac): tidy deal grouping + document merge invariant
claude Jun 23, 2026
695c0de
feat(spac): add recordDealMilestones writer method
claude Jun 23, 2026
d51b229
feat(spac): map 8-K milestone item codes to lifecycle events
claude Jun 23, 2026
edc6e75
feat(spac): wire 8-K milestone mapping into processForm8K
claude Jun 23, 2026
776828b
test(spac): pin report_date precedence; tighten milestone mapping doc
claude Jun 23, 2026
c75f4fa
fix(spac): restore SpacEventType cast for event_type membership check
claude Jun 23, 2026
ea4bbbe
docs(spac): document 8-K milestone population in CLAUDE.md
claude Jun 23, 2026
5cfff5c
docs(spac): refresh event-vocabulary comment for written milestones
claude Jun 23, 2026
502505a
fix(spac): guard 8-K milestone write against empty event_date
claude Jun 23, 2026
783e1cb
feat(spac): add spac_merger_extraction table + repo
claude Jun 23, 2026
0cd3d14
feat(spac): correlate merger extractions + proxy events into deals
claude Jun 23, 2026
302320a
feat(spac): recordMergerProxy + thread extractions through deal recom…
claude Jun 23, 2026
1de72a2
feat(spac): merger-deal extractor + merger-proxy section headings
claude Jun 23, 2026
5636653
feat(spac): processMergerProxy + DEFM14A/PREM14A parse + dispatch wiring
claude Jun 23, 2026
754cc1f
test(spac): end-to-end DEFM14A merger-proxy extraction fixture
claude Jun 23, 2026
bf80496
docs(spac): document merger-proxy extractor in CLAUDE.md
claude Jun 23, 2026
2f4dd59
feat(spac): make section-runner confidence floor per-call tunable
claude Jun 23, 2026
8ce10f7
feat(spac): SEC_MERGER_PROXY_CONFIDENCE_FLOOR for merger extraction
claude Jun 23, 2026
5037039
feat(spac): cover 14C consent + revised proxies in merger-proxy extra…
claude Jun 23, 2026
a765cff
feat(spac): definitive consent statements (DEFM14C) emit the proxy mi…
claude Jun 23, 2026
7ee52a3
docs(spac): document merger-proxy coverage + confidence floor
claude Jun 23, 2026
e05e885
test(spac): cover PRER14A (preliminary revised) extraction-only path
claude Jun 23, 2026
ce95755
feat(sec): slice 8-K primary + EX-99 exhibits from submission
claude Jun 24, 2026
c6406d6
feat(sec): add redemption schema and extractRedemption
claude Jun 24, 2026
0811acb
feat(sec): add redemption model + confidence-floor config
claude Jun 24, 2026
c847d7a
feat(sec): add spac_redemption_extraction storage + DI
claude Jun 24, 2026
d767fac
feat(sec): register the redemption extractor id
claude Jun 24, 2026
50e55e3
feat(sec): correlate redemption extractions onto spac deals
claude Jun 24, 2026
a46527f
feat(sec): read redemption extractions in deal recompute
claude Jun 24, 2026
65cabd5
feat(sec): extract redemptions from 8-K narrative and roll up
claude Jun 24, 2026
8801ca9
feat(sec): escalate known-SPAC trigger 8-Ks to full submission fetch
claude Jun 24, 2026
0331792
test(sec): end-to-end redemption extraction + single rollup
claude Jun 24, 2026
19b57e4
feat(sec): add backfill-redemptions command
claude Jun 24, 2026
16882a6
docs(sec): document redemption extraction + backfill command
claude Jun 24, 2026
5ad4b81
fix(sec): pin version-status test to the full bootstrapped extractor set
claude Jun 24, 2026
a5f7a24
fix(sec): harden redemption extraction from code review
claude Jun 24, 2026
61bce6b
fix(spac): reconcile stale deal rows on recompute
claude Jun 24, 2026
99f0f75
fix(sec): address PR review — test runner, docs, backfill query
claude Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,58 @@ and rolled-up key dates. It is **derived** from two append-only tables — `spac
so replays are idempotent; an `as_of` guard protects filing-sourced scalar fields
from out-of-order writes, and `spac_history` + `ChangeLog` version the row.

Today only the IPO half is populated (S-1/DRS → `registration`, priced 424B1/424B4
→ `ipo`); de-SPAC events (8-K items, S-4/proxy, redemptions, PIPE, de-registration)
are defined-but-deferred slots.
The IPO half is populated from S-1/DRS (`registration`) and priced 424B1/424B4
(`ipo`). De-SPAC **milestone dates** are populated deterministically from 8-K
item codes (known SPACs only — a `spac` row must already exist): item `1.01` →
`definitive_agreement`, `1.02` → `terminated`, `2.01` → `completed`, `5.07` →
`vote`. These group into `spac_deal` attempts via `deriveDeals`
(recomputed from the event stream on every write, so `deal_index` is stable
across replays) and roll up automatically. `target_name`, `pipe_amount`, and
Comment thread
sroussey marked this conversation as resolved.
redemption amounts stay null until the narrative/AI extractors (S-4 / DEFM14A /
425) land — 8-K item codes carry no names or amounts. Still deferred: name/SIC/
ticker transitions and Form 25/15 de-registration.

**Merger proxies** (`DEFM14A`/`PREM14A`, the `DEFM14C`/`PREM14C` consent statements,
and the `DEFR14A`/`PRER14A` revised proxies; extractor id `merger-proxy`) run
`processMergerProxy` (known SPACs only — a `spac` row must already exist): AI
extraction over the merger / business-combination / PIPE sections records a
per-accession `spac_merger_extraction` row (target name/CIK, PIPE amount, merger
consideration) and observes the target company (`relation: "merger-proxy:target"`,
`target_cik` resolved from the canonical company when it has one). `deriveDeals`
correlates each extraction onto the matching `spac_deal` by filing-date window —
*deriving* `target_name` / `target_cik` / `pipe_amount` (a later filing supersedes
an earlier one — definitive over preliminary, revised over definitive), which
retires the 8-K path's positional merge-preserve. Only the **definitive merger**
statements `DEFM14A` and `DEFM14C` emit the `proxy` event (→ `proxy_date` /
`status = proxy`): a consent deal (14C) has no `8-K 5.07` vote, so the definitive
14C is its only approval-stage signal. Preliminary (`PREM14A`/`PREM14C`) and revised
(`DEFR14A`/`PRER14A`) proxies are extraction-only. S-4 is deferred (newco-CIK linkage). Configure the
model via `SEC_MERGER_PROXY_MODEL` (default `claude-sonnet-4-6`) and an optional
confidence floor via `SEC_MERGER_PROXY_CONFIDENCE_FLOOR` (falls back to the shared
`SEC_S1_CONFIDENCE_FLOOR` when unset).

```bash
sec fetch form <cik> DEFM14A # fetch + extract a merger proxy
sec extractor dead-letters merger-proxy # version-fixable extraction failures
sec extractor retry-dead-letters merger-proxy
```

**Redemption actuals** (extractor id `redemption`) are AI-extracted from a known
SPAC's post-vote 8-K narrative. When an 8-K carries item `5.07`, `2.01`, or `8.01`
for a known SPAC, ingestion escalates the fetch to the full submission `.txt` and
reads the primary document + `EX-99.x` exhibits; `processRedemption8K` records a
per-accession `spac_redemption_extraction` row, and `deriveDeals` correlates
`redemption_amount` / `redemption_shares` onto the matching `spac_deal`. The deal
column is the sole source `total_redemption_amount` sums, so redemptions are counted
once. Configure the model via `SEC_REDEMPTION_MODEL` (default `claude-sonnet-4-6`)
and an optional confidence floor via `SEC_REDEMPTION_CONFIDENCE_FLOOR` (falls back to
`SEC_S1_CONFIDENCE_FLOOR`).

```bash
sec spac backfill-redemptions # sweep historical known-SPAC trigger 8-Ks
sec extractor dead-letters redemption # version-fixable extraction failures
sec extractor retry-dead-letters redemption
```

```bash
sec spac report <cik> [--format json] # consolidated report
Expand Down
2 changes: 2 additions & 0 deletions src/cli/groups/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ describe("sec version CLI", () => {
"CFPORTAL",
"D",
"S-1",
"merger-proxy",
"redemption",
]);
const extractorRows = parsed.filter(
(r: { component_kind: string }) => r.component_kind === "extractor"
Expand Down
13 changes: 13 additions & 0 deletions src/commands/spac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

import { Command } from "commander";
import { globalServiceRegistry } from "workglow";
import { withCli } from "@workglow/cli";
import { SpacRepo } from "../storage/spac/SpacRepo";
import { SPAC_SPONSOR_LINK_REPOSITORY_TOKEN } from "../storage/canonical/SpacSponsorLinkSchema";
import { UNDERWRITER_LINK_REPOSITORY_TOKEN } from "../storage/canonical/UnderwriterLinkSchema";
import { BackfillRedemptionsTask } from "../task/spac/BackfillRedemptionsTask";

export interface SpacReport {
readonly cik: number;
Expand Down Expand Up @@ -110,4 +112,15 @@ export function registerSpacCommands(program: Command): void {
);
}
});

spacCmd
.command("backfill-redemptions")
.description("Re-process known-SPAC trigger-item 8-Ks to extract realized redemptions")
.action(async () => {
const out = (await withCli(new BackfillRedemptionsTask()).run({})) as {
selected: number;
processed: number;
};
console.log(`selected ${out.selected} filing(s); processed ${out.processed}`);
});
}
28 changes: 28 additions & 0 deletions src/config/DefaultDI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,16 @@ import {
SpacHistoryPrimaryKeyNames,
SpacHistorySchema,
} from "../storage/spac/SpacHistorySchema";
import {
SPAC_MERGER_EXTRACTION_REPOSITORY_TOKEN,
SpacMergerExtractionPrimaryKeyNames,
SpacMergerExtractionSchema,
} from "../storage/spac/SpacMergerExtractionSchema";
import {
SPAC_REDEMPTION_EXTRACTION_REPOSITORY_TOKEN,
SpacRedemptionExtractionPrimaryKeyNames,
SpacRedemptionExtractionSchema,
} from "../storage/spac/SpacRedemptionExtractionSchema";
import { createStorage } from "./createStorage";

export const DefaultDI = () => {
Expand Down Expand Up @@ -665,6 +675,24 @@ export const DefaultDI = () => {
SPAC_HISTORY_REPOSITORY_TOKEN,
createStorage("spac_history", SpacHistorySchema, SpacHistoryPrimaryKeyNames, [["cik"]])
);
globalServiceRegistry.registerInstance(
SPAC_MERGER_EXTRACTION_REPOSITORY_TOKEN,
createStorage(
"spac_merger_extraction",
SpacMergerExtractionSchema,
SpacMergerExtractionPrimaryKeyNames,
[["cik"]]
)
);
globalServiceRegistry.registerInstance(
SPAC_REDEMPTION_EXTRACTION_REPOSITORY_TOKEN,
createStorage(
"spac_redemption_extraction",
SpacRedemptionExtractionSchema,
SpacRedemptionExtractionPrimaryKeyNames,
[["cik"]]
)
);

// ----- Observation / Canonical / Resolver -----
globalServiceRegistry.registerInstance(
Expand Down
26 changes: 26 additions & 0 deletions src/config/TestingDI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ import {
SpacHistoryPrimaryKeyNames,
SpacHistorySchema,
} from "../storage/spac/SpacHistorySchema";
import {
SPAC_MERGER_EXTRACTION_REPOSITORY_TOKEN,
SpacMergerExtractionPrimaryKeyNames,
SpacMergerExtractionSchema,
} from "../storage/spac/SpacMergerExtractionSchema";
import {
SPAC_REDEMPTION_EXTRACTION_REPOSITORY_TOKEN,
SpacRedemptionExtractionPrimaryKeyNames,
SpacRedemptionExtractionSchema,
} from "../storage/spac/SpacRedemptionExtractionSchema";
import {
CANONICAL_COMPANY_REPOSITORY_TOKEN,
CanonicalCompanyPrimaryKeyNames,
Expand Down Expand Up @@ -493,6 +503,22 @@ export function resetDependencyInjectionsForTesting() {
SPAC_HISTORY_REPOSITORY_TOKEN,
new InMemoryTabularStorage(SpacHistorySchema, SpacHistoryPrimaryKeyNames, [["cik"]])
);
globalServiceRegistry.registerInstance(
SPAC_MERGER_EXTRACTION_REPOSITORY_TOKEN,
new InMemoryTabularStorage(
SpacMergerExtractionSchema,
SpacMergerExtractionPrimaryKeyNames,
[["cik"]]
)
);
globalServiceRegistry.registerInstance(
SPAC_REDEMPTION_EXTRACTION_REPOSITORY_TOKEN,
new InMemoryTabularStorage(
SpacRedemptionExtractionSchema,
SpacRedemptionExtractionPrimaryKeyNames,
[["cik"]]
)
);

// Initialize Crowdfunding repositories
globalServiceRegistry.registerInstance(
Expand Down
4 changes: 4 additions & 0 deletions src/config/setupAllDatabases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import { SPAC_REPOSITORY_TOKEN } from "../storage/spac/SpacSchema";
import { SPAC_DEAL_REPOSITORY_TOKEN } from "../storage/spac/SpacDealSchema";
import { SPAC_EVENT_REPOSITORY_TOKEN } from "../storage/spac/SpacEventSchema";
import { SPAC_HISTORY_REPOSITORY_TOKEN } from "../storage/spac/SpacHistorySchema";
import { SPAC_MERGER_EXTRACTION_REPOSITORY_TOKEN } from "../storage/spac/SpacMergerExtractionSchema";
import { SPAC_REDEMPTION_EXTRACTION_REPOSITORY_TOKEN } from "../storage/spac/SpacRedemptionExtractionSchema";
import {
CANONICAL_COMPANY_ALIAS_REPOSITORY_TOKEN,
CANONICAL_PERSON_ALIAS_REPOSITORY_TOKEN,
Expand Down Expand Up @@ -145,6 +147,8 @@ export async function setupAllDatabases(): Promise<void> {
await globalServiceRegistry.get(SPAC_DEAL_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(SPAC_EVENT_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(SPAC_HISTORY_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(SPAC_MERGER_EXTRACTION_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(SPAC_REDEMPTION_EXTRACTION_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(CIK_LAST_UPDATE_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(PROCESSED_FACTS_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(PROCESSED_SUBMISSIONS_REPOSITORY_TOKEN).setupDatabase();
Expand Down
42 changes: 42 additions & 0 deletions src/sec/forms/miscellaneous-filings/Form_8_K.storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type { ModelConfig } from "workglow";
import { Form8KEventRepo } from "../../../storage/form-8k-event/Form8KEventRepo";
import type { Form8KEvent } from "../../../storage/form-8k-event/Form8KEventSchema";
import type { Form8K } from "./Form_8_K.schema";
import { Form_8_K_ITEMS } from "./Form_8_K";
import { SpacRepo } from "../../../storage/spac/SpacRepo";
import { SpacReportWriter } from "../../../storage/spac/SpacReportWriter";
import { mapItemCodesToSpacEvents } from "./spac8kMilestones";
import { processRedemption8K } from "./redemption8k";

/**
* Extracts item codes from the filing metadata `items` field.
Expand Down Expand Up @@ -48,6 +53,8 @@ export async function processForm8K({
items,
report_date,
form8K,
fullSubmissionText,
model,
}: {
readonly cik: number;
readonly accession_number: string;
Expand All @@ -56,6 +63,8 @@ export async function processForm8K({
readonly items: string | undefined | null;
readonly report_date: string | undefined | null;
readonly form8K: Form8K;
readonly fullSubmissionText?: string;
readonly model?: ModelConfig;
}): Promise<void> {
const eventRepo = new Form8KEventRepo();
const isAmendment = form === "8-K/A";
Expand All @@ -76,4 +85,37 @@ export async function processForm8K({
};
await eventRepo.saveEvent(event);
}

// --- Consolidated SPAC report: map de-SPAC milestone items (known SPACs only) ---
const spacRow = await new SpacRepo().getSpac(cik);
if (spacRow) {
// Skip when no usable date is available: an undated milestone (empty
// event_date) would write junk announced_date/definitive_agreement_date
// onto the deal/row. Reachable only on the best-effort path where the
// filing-metadata row is absent (report_date null, filing_date "").
const eventDate = effectiveReportDate || filing_date;
const spacEvents = eventDate ? mapItemCodesToSpacEvents(itemCodes, eventDate) : [];
if (spacEvents.length > 0) {
await new SpacReportWriter().recordDealMilestones({
cik,
accession_number,
filing_date,
form,
primary_document: null,
events: spacEvents,
});
}
}

if (spacRow && fullSubmissionText) {
await processRedemption8K({
cik,
accession_number,
filing_date,
form,
itemCodes,
fullSubmissionText,
model,
});
}
}
Loading