Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/cli/groups/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe("sec version CLI", () => {
"4",
"424",
"5",
"8-K",
"C",
"CFPORTAL",
"D",
Expand Down
15 changes: 15 additions & 0 deletions src/config/DefaultDI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ import {
VersionEventPrimaryKeyNames,
VersionEventSchema,
} from "../storage/versioning/VersionEventSchema";
import {
FORM_8K_EVENT_REPOSITORY_TOKEN,
Form8KEventPrimaryKeyNames,
Form8KEventSchema,
} from "../storage/form-8k-event/Form8KEventSchema";
import { createStorage } from "./createStorage";

export const DefaultDI = () => {
Expand Down Expand Up @@ -875,4 +880,14 @@ export const DefaultDI = () => {
XBRL_FACT_REPOSITORY_TOKEN,
createStorage("xbrl_fact", XbrlFactRowSchema, XbrlFactPrimaryKeyNames, [["cik"], ["concept"]])
);

// ------------------------------ Form 8-K Events --------------------------------
globalServiceRegistry.registerInstance(
FORM_8K_EVENT_REPOSITORY_TOKEN,
createStorage("form_8k_events", Form8KEventSchema, Form8KEventPrimaryKeyNames, [
["cik", "filing_date"],
["item_code"],

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form_8k_events storage is indexed on ["cik","filing_date"] and ["item_code"], but Form8KEventRepo.getEventsByAccession() queries by { cik, accession_number }. Without an index that includes accession_number this query will likely degrade to a full scan. Add an index such as ["cik","accession_number"] (and optionally ["accession_number"] / ["cik"] depending on expected query patterns).

Suggested change
["item_code"],
["item_code"],
["cik", "accession_number"],

Copilot uses AI. Check for mistakes.
["accession_number"],
])
);
};
15 changes: 15 additions & 0 deletions src/config/TestingDI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ import {
XbrlFactPrimaryKeyNames,
XbrlFactRowSchema,
} from "../storage/xbrl/XbrlFactSchema";
import {
FORM_8K_EVENT_REPOSITORY_TOKEN,
Form8KEventPrimaryKeyNames,
Form8KEventSchema,
} from "../storage/form-8k-event/Form8KEventSchema";
import {
CANONICAL_COMPANY_REPOSITORY_TOKEN,
CanonicalCompanyPrimaryKeyNames,
Expand Down Expand Up @@ -781,4 +786,14 @@ export function resetDependencyInjectionsForTesting() {
XBRL_FACT_REPOSITORY_TOKEN,
new InMemoryTabularStorage(XbrlFactRowSchema, XbrlFactPrimaryKeyNames, [["cik"], ["concept"]])
);

// Form 8-K Events
globalServiceRegistry.registerInstance(
FORM_8K_EVENT_REPOSITORY_TOKEN,
new InMemoryTabularStorage(Form8KEventSchema, Form8KEventPrimaryKeyNames, [
["cik", "filing_date"],
["item_code"],

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The in-memory Form 8-K event storage is indexed on ["cik","filing_date"] and ["item_code"], but tests/repo APIs query by { cik, accession_number }. Add an index including accession_number (e.g. ["cik","accession_number"]) so getEventsByAccession() doesn't require scanning all rows.

Suggested change
["item_code"],
["item_code"],
["cik", "accession_number"],

Copilot uses AI. Check for mistakes.
["accession_number"],
])
);
}
2 changes: 2 additions & 0 deletions src/config/resetAllDatabases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
CANONICAL_PERSON_ALIAS_REPOSITORY_TOKEN,
} from "../storage/canonical/CanonicalAliasSchemas";
import { CANONICAL_COMPANY_REPOSITORY_TOKEN } from "../storage/canonical/CanonicalCompanySchema";
import { FORM_8K_EVENT_REPOSITORY_TOKEN } from "../storage/form-8k-event/Form8KEventSchema";
import {
CANONICAL_COMPANY_ADDRESS_REPOSITORY_TOKEN,
CANONICAL_COMPANY_PHONE_REPOSITORY_TOKEN,
Expand Down Expand Up @@ -112,4 +113,5 @@ export async function resetAllDatabases(): Promise<void> {
await globalServiceRegistry.get(CANONICAL_COMPANY_PHONE_REPOSITORY_TOKEN).deleteAll();
await globalServiceRegistry.get(CANONICAL_PERSON_ALIAS_REPOSITORY_TOKEN).deleteAll();
await globalServiceRegistry.get(CANONICAL_COMPANY_ALIAS_REPOSITORY_TOKEN).deleteAll();
await globalServiceRegistry.get(FORM_8K_EVENT_REPOSITORY_TOKEN).deleteAll();
}
2 changes: 2 additions & 0 deletions src/config/setupAllDatabases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { UNDERWRITER_FAMILY_MEMBERSHIP_REPOSITORY_TOKEN } from "../storage/canon
import { UNDERWRITER_LINK_REPOSITORY_TOKEN } from "../storage/canonical/UnderwriterLinkSchema";
import { USE_OF_PROCEEDS_REPOSITORY_TOKEN } from "../storage/use-of-proceeds/UseOfProceedsSchema";
import { XBRL_FACT_REPOSITORY_TOKEN } from "../storage/xbrl/XbrlFactSchema";
import { FORM_8K_EVENT_REPOSITORY_TOKEN } from "../storage/form-8k-event/Form8KEventSchema";
import { CANONICAL_COMPANY_REPOSITORY_TOKEN } from "../storage/canonical/CanonicalCompanySchema";
import {
CANONICAL_COMPANY_ADDRESS_REPOSITORY_TOKEN,
Expand Down Expand Up @@ -175,6 +176,7 @@ export async function setupAllDatabases(): Promise<void> {
await globalServiceRegistry.get(UNDERWRITER_LINK_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(USE_OF_PROCEEDS_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(XBRL_FACT_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(FORM_8K_EVENT_REPOSITORY_TOKEN).setupDatabase();
// View DDL is created here only on the SQLite path; the Postgres backend
// owns its own view bootstrap (and getDb() now throws when SEC_DB_TYPE
// isn't sqlite). Tests use the in-memory backend where views don't apply.
Expand Down
61 changes: 61 additions & 0 deletions src/sec/forms/miscellaneous-filings/Form_8_K.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @license
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*/

import { Type, Static } from "typebox";
import { SCHEMA_VERSION_TYPE, CIK_TYPE } from "../FormSchemaUtil";

export const SubTypeList = Type.Union([Type.Literal("8-K"), Type.Literal("8-K/A")], {
description: "Submission Type Form",
});

const SIGNATURE_TYPE = Type.Object({
signatureName: Type.String({ minLength: 1, maxLength: 150 }),
signatureTitle: Type.Optional(Type.String({ maxLength: 150 })),
signatureDate: Type.Optional(Type.String()),
});

export type Form8KSignature = Static<typeof SIGNATURE_TYPE>;

const SIGNATURE_BLOCK_TYPE = Type.Object({
signature: Type.Union([SIGNATURE_TYPE, Type.Array(SIGNATURE_TYPE)]),
});

const FILER_INFO_TYPE = Type.Object({
filerCik: Type.Optional(CIK_TYPE),
filerCcc: Type.Optional(Type.String({ maxLength: 8 })),
});

const HEADER_DATA_TYPE = Type.Object({
filerInfo: Type.Optional(FILER_INFO_TYPE),
});

const FORM_DATA_TYPE = Type.Object({
items: Type.Optional(
Type.Object({
item: Type.Union([Type.String(), Type.Array(Type.String())]),
})
),
periodOfReport: Type.Optional(Type.String()),
signatureBlock: Type.Optional(SIGNATURE_BLOCK_TYPE),
});

/**
* Schema for 8-K filings submitted as structured XML through EDGAR.
*/
export const Form8KSchema = Type.Object({
schemaVersion: Type.Optional(SCHEMA_VERSION_TYPE),
submissionType: Type.Optional(SubTypeList),
headerData: Type.Optional(HEADER_DATA_TYPE),
formData: Type.Optional(FORM_DATA_TYPE),
});

export type Form8K = Static<typeof Form8KSchema>;

export const Form8KSubmissionSchema = Type.Object({
edgarSubmission: Form8KSchema,
});

export type Form8KSubmission = Static<typeof Form8KSubmissionSchema>;
79 changes: 79 additions & 0 deletions src/sec/forms/miscellaneous-filings/Form_8_K.storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*/

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";

/**
* Extracts item codes from the filing metadata `items` field.
* The items field is a comma-separated string of item codes (e.g., "2.02,9.01").
* Also merges any items found in the parsed XML form data.
*/
function extractItemCodes(filingItems: string | undefined | null, form8K: Form8K): string[] {
const itemSet = new Set<string>();

if (filingItems) {
for (const raw of filingItems.split(/[,;]/)) {
const item = raw.trim();
if (item) {
itemSet.add(item);
}
}
}

if (form8K.formData?.items?.item) {
const xmlItems = form8K.formData.items.item;
const itemArray = Array.isArray(xmlItems) ? xmlItems : [xmlItems];
for (const item of itemArray) {
const trimmed = item.trim();
if (trimmed) {
itemSet.add(trimmed);
}
}
}

return [...itemSet].sort();
}

export async function processForm8K({
cik,
accession_number,
filing_date,
form,
items,
report_date,
form8K,
}: {
readonly cik: number;
readonly accession_number: string;
readonly filing_date: string;
readonly form: string;
readonly items: string | undefined | null;
readonly report_date: string | undefined | null;
readonly form8K: Form8K;
}): Promise<void> {
const eventRepo = new Form8KEventRepo();
const isAmendment = form === "8-K/A";

const effectiveReportDate = form8K.formData?.periodOfReport || report_date || null;

const itemCodes = extractItemCodes(items, form8K);

for (const itemCode of itemCodes) {
const event: Form8KEvent = {
cik,
accession_number,
item_code: itemCode,
item_description: Form_8_K_ITEMS[itemCode] ?? null,
filing_date,
report_date: effectiveReportDate,
is_amendment: isAmendment,
};
await eventRepo.saveEvent(event);
}
}
Loading