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
6 changes: 3 additions & 3 deletions src/cli/groups/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ describe("sec version CLI", () => {
const status = await runCli(["version", "status", "--format", "json"], dir);
expect(status.exitCode).toBe(0);
const parsed = JSON.parse(status.stdout);
// PR2 wires bootstrapExtractorVersions() into db setup; expect the
// five extractor ids registered at 1.0.0 in the current slot.
// PR2 wires bootstrapExtractorVersions() into db setup; expect every
// extractor id registered at 1.0.0 in the current slot.
const ids = parsed
.filter((r: { component_kind: string }) => r.component_kind === "extractor")
.map((r: { component_id: string }) => r.component_id)
.sort();
expect(ids).toEqual(["1-A", "1-K", "1-Z", "C", "D"]);
expect(ids).toEqual(["1-A", "1-K", "1-Z", "144", "3", "4", "5", "C", "D"]);
const extractorRows = parsed.filter(
(r: { component_kind: string }) => r.component_kind === "extractor"
);
Expand Down
16 changes: 16 additions & 0 deletions src/cli/queries/DbStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import { INVESTMENT_OFFERING_REPOSITORY_TOKEN } from "../../storage/investment-o
import { PHONE_REPOSITORY_TOKEN } from "../../storage/phone/PhoneSchema";
import { CROWDFUNDING_REPOSITORY_TOKEN } from "../../storage/portal/CrowdfundingSchema";
import { PORTAL_REPOSITORY_TOKEN } from "../../storage/portal/PortalSchema";
import {
SECTION16_FILING_REPOSITORY_TOKEN,
SECTION16_HOLDING_REPOSITORY_TOKEN,
SECTION16_TRANSACTION_REPOSITORY_TOKEN,
} from "../../storage/section16/Section16Schema";
import {
FORM144_ACQUISITION_REPOSITORY_TOKEN,
FORM144_FILING_REPOSITORY_TOKEN,
FORM144_RECENT_SALE_REPOSITORY_TOKEN,
} from "../../storage/form144/Form144Schema";
import { PROCESSED_FACTS_REPOSITORY_TOKEN } from "../../storage/processing/ProcessedFactsSchema";
import { PROCESSED_SUBMISSIONS_REPOSITORY_TOKEN } from "../../storage/processing/ProcessedSubmissionsSchema";
import { EXTRACTOR_RUN_REPOSITORY_TOKEN } from "../../storage/versioning/ExtractorRunSchema";
Expand Down Expand Up @@ -90,6 +100,12 @@ const TABLE_TOKENS: ReadonlyArray<{
{ table: "canonical_company", token: CANONICAL_COMPANY_REPOSITORY_TOKEN as any },
{ table: "person_identity_link", token: PERSON_IDENTITY_LINK_REPOSITORY_TOKEN as any },
{ table: "company_identity_link", token: COMPANY_IDENTITY_LINK_REPOSITORY_TOKEN as any },
{ table: "section16_filings", token: SECTION16_FILING_REPOSITORY_TOKEN as any },
{ table: "section16_transactions", token: SECTION16_TRANSACTION_REPOSITORY_TOKEN as any },
{ table: "section16_holdings", token: SECTION16_HOLDING_REPOSITORY_TOKEN as any },
{ table: "form144_filings", token: FORM144_FILING_REPOSITORY_TOKEN as any },
{ table: "form144_acquisitions", token: FORM144_ACQUISITION_REPOSITORY_TOKEN as any },
{ table: "form144_recent_sales", token: FORM144_RECENT_SALE_REPOSITORY_TOKEN as any },
];

export async function getDbStats(): Promise<TableStat[]> {
Expand Down
72 changes: 72 additions & 0 deletions src/config/DefaultDI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ import {
PortalPrimaryKeyNames,
PortalSchema,
} from "../storage/portal/PortalSchema";
import {
SECTION16_FILING_REPOSITORY_TOKEN,
SECTION16_HOLDING_REPOSITORY_TOKEN,
SECTION16_TRANSACTION_REPOSITORY_TOKEN,
Section16FilingPrimaryKeyNames,
Section16FilingSchema,
Section16HoldingPrimaryKeyNames,
Section16HoldingSchema,
Section16TransactionPrimaryKeyNames,
Section16TransactionSchema,
} from "../storage/section16/Section16Schema";
import {
FORM144_ACQUISITION_REPOSITORY_TOKEN,
FORM144_FILING_REPOSITORY_TOKEN,
FORM144_RECENT_SALE_REPOSITORY_TOKEN,
Form144AcquisitionPrimaryKeyNames,
Form144AcquisitionSchema,
Form144FilingPrimaryKeyNames,
Form144FilingSchema,
Form144RecentSalePrimaryKeyNames,
Form144RecentSaleSchema,
} from "../storage/form144/Form144Schema";
import {
CIK_LAST_UPDATE_REPOSITORY_TOKEN,
CikLastUpdatePrimaryKeyNames,
Expand Down Expand Up @@ -357,6 +379,56 @@ export const DefaultDI = () => {
)
);

// ------------------------------ Section 16 (Forms 3/4/5) --------------------------------
globalServiceRegistry.registerInstance(
SECTION16_FILING_REPOSITORY_TOKEN,
createStorage("section16_filings", Section16FilingSchema, Section16FilingPrimaryKeyNames, [
["issuer_cik"],
["form"],
])
);
globalServiceRegistry.registerInstance(
SECTION16_TRANSACTION_REPOSITORY_TOKEN,
createStorage(
"section16_transactions",
Section16TransactionSchema,
Section16TransactionPrimaryKeyNames,
[["accession_number"], ["issuer_cik"]]
)
);
globalServiceRegistry.registerInstance(
SECTION16_HOLDING_REPOSITORY_TOKEN,
createStorage("section16_holdings", Section16HoldingSchema, Section16HoldingPrimaryKeyNames, [
["accession_number"],
["issuer_cik"],
])
);

// ------------------------------ Form 144 --------------------------------
globalServiceRegistry.registerInstance(
FORM144_FILING_REPOSITORY_TOKEN,
createStorage("form144_filings", Form144FilingSchema, Form144FilingPrimaryKeyNames, [
["issuer_cik"],
["form"],
])
);
globalServiceRegistry.registerInstance(
FORM144_ACQUISITION_REPOSITORY_TOKEN,
createStorage(
"form144_acquisitions",
Form144AcquisitionSchema,
Form144AcquisitionPrimaryKeyNames,
[["accession_number"], ["issuer_cik"]]
)
);
globalServiceRegistry.registerInstance(
FORM144_RECENT_SALE_REPOSITORY_TOKEN,
createStorage("form144_recent_sales", Form144RecentSaleSchema, Form144RecentSalePrimaryKeyNames, [
["accession_number"],
["issuer_cik"],
])
);

// ------------------------------ Change Log --------------------------------
globalServiceRegistry.registerInstance(
CHANGE_LOG_REPOSITORY_TOKEN,
Expand Down
69 changes: 69 additions & 0 deletions src/config/TestingDI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ import {
PortalPrimaryKeyNames,
PortalSchema,
} from "../storage/portal/PortalSchema";
import {
SECTION16_FILING_REPOSITORY_TOKEN,
SECTION16_HOLDING_REPOSITORY_TOKEN,
SECTION16_TRANSACTION_REPOSITORY_TOKEN,
Section16FilingPrimaryKeyNames,
Section16FilingSchema,
Section16HoldingPrimaryKeyNames,
Section16HoldingSchema,
Section16TransactionPrimaryKeyNames,
Section16TransactionSchema,
} from "../storage/section16/Section16Schema";
import {
FORM144_ACQUISITION_REPOSITORY_TOKEN,
FORM144_FILING_REPOSITORY_TOKEN,
FORM144_RECENT_SALE_REPOSITORY_TOKEN,
Form144AcquisitionPrimaryKeyNames,
Form144AcquisitionSchema,
Form144FilingPrimaryKeyNames,
Form144FilingSchema,
Form144RecentSalePrimaryKeyNames,
Form144RecentSaleSchema,
} from "../storage/form144/Form144Schema";
import {
CIK_LAST_UPDATE_REPOSITORY_TOKEN,
CikLastUpdatePrimaryKeyNames,
Expand Down Expand Up @@ -364,6 +386,53 @@ export function resetDependencyInjectionsForTesting() {
])
);

// Initialize Section 16 (Forms 3/4/5) repositories
globalServiceRegistry.registerInstance(
SECTION16_FILING_REPOSITORY_TOKEN,
new InMemoryTabularStorage(Section16FilingSchema, Section16FilingPrimaryKeyNames, [
["issuer_cik"],
["form"],
])
);
globalServiceRegistry.registerInstance(
SECTION16_TRANSACTION_REPOSITORY_TOKEN,
new InMemoryTabularStorage(
Section16TransactionSchema,
Section16TransactionPrimaryKeyNames,
[["accession_number"], ["issuer_cik"]]
)
);
globalServiceRegistry.registerInstance(
SECTION16_HOLDING_REPOSITORY_TOKEN,
new InMemoryTabularStorage(Section16HoldingSchema, Section16HoldingPrimaryKeyNames, [
["accession_number"],
["issuer_cik"],
])
);

// Initialize Form 144 repositories
globalServiceRegistry.registerInstance(
FORM144_FILING_REPOSITORY_TOKEN,
new InMemoryTabularStorage(Form144FilingSchema, Form144FilingPrimaryKeyNames, [
["issuer_cik"],
["form"],
])
);
globalServiceRegistry.registerInstance(
FORM144_ACQUISITION_REPOSITORY_TOKEN,
new InMemoryTabularStorage(Form144AcquisitionSchema, Form144AcquisitionPrimaryKeyNames, [
["accession_number"],
["issuer_cik"],
])
);
globalServiceRegistry.registerInstance(
FORM144_RECENT_SALE_REPOSITORY_TOKEN,
new InMemoryTabularStorage(Form144RecentSaleSchema, Form144RecentSalePrimaryKeyNames, [
["accession_number"],
["issuer_cik"],
])
);

// Initialize Processing Tracking repositories
globalServiceRegistry.registerInstance(
CIK_LAST_UPDATE_REPOSITORY_TOKEN,
Expand Down
16 changes: 16 additions & 0 deletions src/config/setupAllDatabases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ import {
CROWDFUNDING_REPOSITORY_TOKEN,
} from "../storage/portal/CrowdfundingSchema";
import { PORTAL_REPOSITORY_TOKEN } from "../storage/portal/PortalSchema";
import {
SECTION16_FILING_REPOSITORY_TOKEN,
SECTION16_HOLDING_REPOSITORY_TOKEN,
SECTION16_TRANSACTION_REPOSITORY_TOKEN,
} from "../storage/section16/Section16Schema";
import {
FORM144_ACQUISITION_REPOSITORY_TOKEN,
FORM144_FILING_REPOSITORY_TOKEN,
FORM144_RECENT_SALE_REPOSITORY_TOKEN,
} from "../storage/form144/Form144Schema";
import { CIK_LAST_UPDATE_REPOSITORY_TOKEN } from "../storage/processing/CikLastUpdateSchema";
import { PROCESSED_FACTS_REPOSITORY_TOKEN } from "../storage/processing/ProcessedFactsSchema";
import { PROCESSED_SUBMISSIONS_REPOSITORY_TOKEN } from "../storage/processing/ProcessedSubmissionsSchema";
Expand Down Expand Up @@ -95,6 +105,12 @@ export async function setupAllDatabases(): Promise<void> {
await globalServiceRegistry.get(CROWDFUNDING_OFFERINGS_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(CROWDFUNDING_REPORTS_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(CROWDFUNDING_HISTORY_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(SECTION16_FILING_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(SECTION16_TRANSACTION_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(SECTION16_HOLDING_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(FORM144_FILING_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(FORM144_ACQUISITION_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(FORM144_RECENT_SALE_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(CHANGE_LOG_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(PORTAL_REPOSITORY_TOKEN).setupDatabase();
await globalServiceRegistry.get(REGA_OFFERING_REPOSITORY_TOKEN).setupDatabase();
Expand Down
129 changes: 129 additions & 0 deletions src/sec/forms/insider-trading/Form_144.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* @license
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*/

// Schema for the EDGAR Form 144 (and 144/A) "Notice of Proposed Sale of
// Securities" XML. Since 2022 these are filed electronically under the
// shared edgar/ownership namespace, with a `com:` common namespace for
// address parts. The parser runs with removeNSPrefix, so `com:street1`
// arrives as `street1`.
//
// Unlike the ownership forms (3/4/5), Form 144 leaves are plain text rather
// than `{ value }` wrappers, and dates are US-format strings (MM/DD/YYYY),
// stored verbatim rather than coerced to ISO.

import { Static, Type } from "typebox";

const ADDRESS_TYPE = Type.Object({
street1: Type.Optional(Type.String()),
street2: Type.Optional(Type.String()),
city: Type.Optional(Type.String()),
stateOrCountry: Type.Optional(Type.String()),
zipCode: Type.Optional(Type.String()),
});

const FILER_CREDENTIALS_TYPE = Type.Object({
cik: Type.Optional(Type.String()),
ccc: Type.Optional(Type.String()),
});

const FILER_INFO_TYPE = Type.Object({
filer: Type.Optional(
Type.Object({
filerCredentials: Type.Optional(FILER_CREDENTIALS_TYPE),
})
),
liveTestFlag: Type.Optional(Type.String()),
});

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

const RELATIONSHIPS_TO_ISSUER_TYPE = Type.Object({
relationshipToIssuer: Type.Optional(Type.Array(Type.String())),
});

const ISSUER_INFO_TYPE = Type.Object({
issuerCik: Type.Optional(Type.String()),
issuerName: Type.Optional(Type.String()),
secFileNumber: Type.Optional(Type.String()),
issuerAddress: Type.Optional(ADDRESS_TYPE),
issuerContactPhone: Type.Optional(Type.String()),
nameOfPersonForWhoseAccountTheSecuritiesAreToBeSold: Type.Optional(Type.String()),
relationshipsToIssuer: Type.Optional(RELATIONSHIPS_TO_ISSUER_TYPE),
});

const BROKER_DETAILS_TYPE = Type.Object({
name: Type.Optional(Type.String()),
address: Type.Optional(ADDRESS_TYPE),
});

const SECURITIES_INFORMATION_TYPE = Type.Object({
securitiesClassTitle: Type.Optional(Type.String()),
brokerOrMarketmakerDetails: Type.Optional(BROKER_DETAILS_TYPE),
// Numeric leaves are kept as raw strings and coerced in storage. Typing them
// as Type.Number() would let Value.Convert turn an empty element ("") into a
// fabricated 0, indistinguishable from a real zero.
noOfUnitsSold: Type.Optional(Type.String()),
aggregateMarketValue: Type.Optional(Type.String()),
noOfUnitsOutstanding: Type.Optional(Type.String()),
approxSaleDate: Type.Optional(Type.String()),
securitiesExchangeName: Type.Optional(Type.String()),
});

const SECURITIES_TO_BE_SOLD_TYPE = Type.Object({
securitiesClassTitle: Type.Optional(Type.String()),
acquiredDate: Type.Optional(Type.String()),
natureOfAcquisitionTransaction: Type.Optional(Type.String()),
nameOfPersonfromWhomAcquired: Type.Optional(Type.String()),
isGiftTransaction: Type.Optional(Type.String()),
amountOfSecuritiesAcquired: Type.Optional(Type.String()),
paymentDate: Type.Optional(Type.String()),
natureOfPayment: Type.Optional(Type.String()),
});

const SELLER_DETAILS_TYPE = Type.Object({
name: Type.Optional(Type.String()),
address: Type.Optional(ADDRESS_TYPE),
});

const SECURITIES_SOLD_PAST_3_MONTHS_TYPE = Type.Object({
sellerDetails: Type.Optional(SELLER_DETAILS_TYPE),
securitiesClassTitle: Type.Optional(Type.String()),
saleDate: Type.Optional(Type.String()),
amountOfSecuritiesSold: Type.Optional(Type.String()),
grossProceeds: Type.Optional(Type.String()),
});

const NOTICE_SIGNATURE_TYPE = Type.Object({
noticeDate: Type.Optional(Type.String()),
signature: Type.Optional(Type.String()),
});

const FORM_DATA_TYPE = Type.Object({
issuerInfo: Type.Optional(ISSUER_INFO_TYPE),
securitiesInformation: Type.Optional(SECURITIES_INFORMATION_TYPE),
securitiesToBeSold: Type.Optional(Type.Array(SECURITIES_TO_BE_SOLD_TYPE)),
nothingToReportFlagOnSecuritiesSoldInPast3Months: Type.Optional(Type.String()),
securitiesSoldInPast3Months: Type.Optional(Type.Array(SECURITIES_SOLD_PAST_3_MONTHS_TYPE)),
noticeSignature: Type.Optional(NOTICE_SIGNATURE_TYPE),
});

export const Form144Schema = Type.Object({
headerData: Type.Optional(HEADER_DATA_TYPE),
formData: Type.Optional(FORM_DATA_TYPE),
});

export type Form144 = Static<typeof Form144Schema>;

// Wrapper matching the XML root element so the parser's array-path detection
// produces `edgarSubmission.*` jpaths.
export const Form144SubmissionSchema = Type.Object({
edgarSubmission: Form144Schema,
});

export type Form144Submission = Static<typeof Form144SubmissionSchema>;
Loading