Automate issuer overlay creation from ticker searches

This commit is contained in:
2026-03-19 20:44:58 -04:00
parent 17de3dd72d
commit 391d6d34ce
79 changed files with 4746 additions and 695 deletions

View File

@@ -9,7 +9,7 @@ import { companyFinancialBundle } from '@/lib/server/db/schema';
export const CURRENT_COMPANY_FINANCIAL_BUNDLE_VERSION = 15;
export type CompanyFinancialBundleRecord = {
type CompanyFinancialBundleRecord = {
id: number;
ticker: string;
surface_kind: FinancialSurfaceKind;
@@ -107,6 +107,6 @@ export async function deleteCompanyFinancialBundlesForTicker(ticker: string) {
.where(eq(companyFinancialBundle.ticker, ticker.trim().toUpperCase()));
}
export const __companyFinancialBundlesInternals = {
const __companyFinancialBundlesInternals = {
BUNDLE_VERSION: CURRENT_COMPANY_FINANCIAL_BUNDLE_VERSION
};

View File

@@ -6,7 +6,7 @@ import { companyOverviewCache, schema } from '@/lib/server/db/schema';
export const CURRENT_COMPANY_OVERVIEW_CACHE_VERSION = 1;
export type CompanyOverviewCacheRecord = {
type CompanyOverviewCacheRecord = {
id: number;
user_id: string;
ticker: string;
@@ -86,7 +86,7 @@ export async function upsertCompanyOverviewCache(input: {
return toRecord(saved);
}
export async function deleteCompanyOverviewCache(input: { userId: string; ticker: string }) {
async function deleteCompanyOverviewCache(input: { userId: string; ticker: string }) {
const normalizedTicker = input.ticker.trim().toUpperCase();
return await getDb()
@@ -97,6 +97,6 @@ export async function deleteCompanyOverviewCache(input: { userId: string; ticker
));
}
export const __companyOverviewCacheInternals = {
const __companyOverviewCacheInternals = {
CACHE_VERSION: CURRENT_COMPANY_OVERVIEW_CACHE_VERSION
};

View File

@@ -80,7 +80,7 @@ export type DimensionStatementBundle = {
statements: Record<FinancialStatementKind, DimensionStatementSnapshotRow[]>;
};
export type FilingStatementSnapshotRecord = {
type FilingStatementSnapshotRecord = {
id: number;
filing_id: number;
ticker: string;
@@ -97,7 +97,7 @@ export type FilingStatementSnapshotRecord = {
updated_at: string;
};
export type UpsertFilingStatementSnapshotInput = {
type UpsertFilingStatementSnapshotInput = {
filing_id: number;
ticker: string;
filing_date: string;
@@ -191,7 +191,7 @@ export async function upsertFilingStatementSnapshot(
return toSnapshotRecord(saved);
}
export async function listFilingStatementSnapshotsByTicker(input: {
async function listFilingStatementSnapshotsByTicker(input: {
ticker: string;
window: "10y" | "all";
limit?: number;
@@ -235,7 +235,7 @@ export async function listFilingStatementSnapshotsByTicker(input: {
};
}
export async function countFilingStatementSnapshotStatuses(ticker: string) {
async function countFilingStatementSnapshotStatuses(ticker: string) {
const rows = await db
.select({
status: filingStatementSnapshot.parse_status,

View File

@@ -149,6 +149,10 @@ describe("filing taxonomy snapshot normalization", () => {
kpi_row_count: 1,
unmapped_row_count: 0,
material_unmapped_row_count: 0,
residual_primary_count: 0,
residual_disclosure_count: 0,
unsupported_concept_count: 0,
issuer_overlay_match_count: 0,
warnings: ["legacy_warning"],
},
facts_count: 3,
@@ -223,6 +227,10 @@ describe("filing taxonomy snapshot normalization", () => {
kpiRowCount: 1,
unmappedRowCount: 0,
materialUnmappedRowCount: 0,
residualPrimaryCount: 0,
residualDisclosureCount: 0,
unsupportedConceptCount: 0,
issuerOverlayMatchCount: 0,
warnings: ["legacy_warning"],
});
});
@@ -338,6 +346,10 @@ describe("filing taxonomy snapshot normalization", () => {
kpiRowCount: 0,
unmapped_row_count: 0,
materialUnmappedRowCount: 0,
residualPrimaryCount: 0,
residualDisclosureCount: 0,
unsupportedConceptCount: 0,
issuerOverlayMatchCount: 0,
warnings: [],
},
});
@@ -355,6 +367,10 @@ describe("filing taxonomy snapshot normalization", () => {
kpiRowCount: 0,
unmappedRowCount: 0,
materialUnmappedRowCount: 0,
residualPrimaryCount: 0,
residualDisclosureCount: 0,
unsupportedConceptCount: 0,
issuerOverlayMatchCount: 0,
warnings: [],
});
expect(normalized.computed_definitions).toEqual([

View File

@@ -73,6 +73,7 @@ export type FilingTaxonomySnapshotRecord = {
derived_metrics: Filing["metrics"];
validation_result: MetricValidationResult | null;
normalization_summary: NormalizationSummary | null;
issuer_overlay_revision_id: number | null;
facts_count: number;
concepts_count: number;
dimensions_count: number;
@@ -80,7 +81,7 @@ export type FilingTaxonomySnapshotRecord = {
updated_at: string;
};
export type FilingTaxonomyContextRecord = {
type FilingTaxonomyContextRecord = {
id: number;
snapshot_id: number;
context_id: string;
@@ -94,7 +95,7 @@ export type FilingTaxonomyContextRecord = {
created_at: string;
};
export type FilingTaxonomyAssetRecord = {
type FilingTaxonomyAssetRecord = {
id: number;
snapshot_id: number;
asset_type: FilingTaxonomyAssetType;
@@ -133,7 +134,7 @@ export type FilingTaxonomyConceptRecord = {
created_at: string;
};
export type FilingTaxonomyFactRecord = {
type FilingTaxonomyFactRecord = {
id: number;
snapshot_id: number;
concept_key: string;
@@ -164,7 +165,7 @@ export type FilingTaxonomyFactRecord = {
created_at: string;
};
export type FilingTaxonomyMetricValidationRecord = {
type FilingTaxonomyMetricValidationRecord = {
id: number;
snapshot_id: number;
metric_key: keyof NonNullable<Filing["metrics"]>;
@@ -182,7 +183,7 @@ export type FilingTaxonomyMetricValidationRecord = {
updated_at: string;
};
export type UpsertFilingTaxonomySnapshotInput = {
type UpsertFilingTaxonomySnapshotInput = {
filing_id: number;
ticker: string;
filing_date: string;
@@ -204,6 +205,7 @@ export type UpsertFilingTaxonomySnapshotInput = {
derived_metrics: Filing["metrics"];
validation_result: MetricValidationResult | null;
normalization_summary: NormalizationSummary | null;
issuer_overlay_revision_id?: number | null;
facts_count: number;
concepts_count: number;
dimensions_count: number;
@@ -294,6 +296,7 @@ const FINANCIAL_STATEMENT_KINDS = [
"income",
"balance",
"cash_flow",
"disclosure",
"equity",
"comprehensive_income",
] as const satisfies FinancialStatementKind[];
@@ -351,6 +354,7 @@ function asStatementKind(value: unknown): FinancialStatementKind | null {
return value === "income" ||
value === "balance" ||
value === "cash_flow" ||
value === "disclosure" ||
value === "equity" ||
value === "comprehensive_income"
? value
@@ -576,7 +580,9 @@ function normalizeSurfaceRows(
if (
normalizedStatement === "income" ||
normalizedStatement === "balance" ||
normalizedStatement === "cash_flow"
normalizedStatement === "cash_flow" ||
normalizedStatement === "equity" ||
normalizedStatement === "disclosure"
) {
normalizedRow.statement = normalizedStatement;
}
@@ -856,6 +862,17 @@ function normalizeNormalizationSummary(value: unknown) {
asNumber(
row.materialUnmappedRowCount ?? row.material_unmapped_row_count,
) ?? 0,
residualPrimaryCount:
asNumber(row.residualPrimaryCount ?? row.residual_primary_count) ?? 0,
residualDisclosureCount:
asNumber(row.residualDisclosureCount ?? row.residual_disclosure_count) ??
0,
unsupportedConceptCount:
asNumber(row.unsupportedConceptCount ?? row.unsupported_concept_count) ??
0,
issuerOverlayMatchCount:
asNumber(row.issuerOverlayMatchCount ?? row.issuer_overlay_match_count) ??
0,
warnings: normalizeStringArray(row.warnings),
} satisfies NormalizationSummary;
}
@@ -962,6 +979,7 @@ function toSnapshotRecord(
derived_metrics: row.derived_metrics ?? null,
validation_result: row.validation_result ?? null,
normalization_summary: normalized.normalization_summary,
issuer_overlay_revision_id: row.issuer_overlay_revision_id ?? null,
facts_count: row.facts_count,
concepts_count: row.concepts_count,
dimensions_count: row.dimensions_count,
@@ -1107,7 +1125,7 @@ export async function getFilingTaxonomySnapshotByFilingId(filingId: number) {
return row ? toSnapshotRecord(row) : null;
}
export async function listFilingTaxonomyAssets(snapshotId: number) {
async function listFilingTaxonomyAssets(snapshotId: number) {
const rows = await db
.select()
.from(filingTaxonomyAsset)
@@ -1117,7 +1135,7 @@ export async function listFilingTaxonomyAssets(snapshotId: number) {
return rows.map(toAssetRecord);
}
export async function listFilingTaxonomyContexts(snapshotId: number) {
async function listFilingTaxonomyContexts(snapshotId: number) {
const rows = await db
.select()
.from(filingTaxonomyContext)
@@ -1127,7 +1145,7 @@ export async function listFilingTaxonomyContexts(snapshotId: number) {
return rows.map(toContextRecord);
}
export async function listFilingTaxonomyConcepts(snapshotId: number) {
async function listFilingTaxonomyConcepts(snapshotId: number) {
const rows = await db
.select()
.from(filingTaxonomyConcept)
@@ -1137,7 +1155,7 @@ export async function listFilingTaxonomyConcepts(snapshotId: number) {
return rows.map(toConceptRecord);
}
export async function listFilingTaxonomyFacts(snapshotId: number) {
async function listFilingTaxonomyFacts(snapshotId: number) {
const rows = await db
.select()
.from(filingTaxonomyFact)
@@ -1147,7 +1165,7 @@ export async function listFilingTaxonomyFacts(snapshotId: number) {
return rows.map(toFactRecord);
}
export async function listFilingTaxonomyMetricValidations(snapshotId: number) {
async function listFilingTaxonomyMetricValidations(snapshotId: number) {
const rows = await db
.select()
.from(filingTaxonomyMetricValidation)
@@ -1188,6 +1206,7 @@ export async function upsertFilingTaxonomySnapshot(
derived_metrics: input.derived_metrics,
validation_result: input.validation_result,
normalization_summary: normalized.normalization_summary,
issuer_overlay_revision_id: input.issuer_overlay_revision_id ?? null,
facts_count: input.facts_count,
concepts_count: input.concepts_count,
dimensions_count: input.dimensions_count,
@@ -1217,6 +1236,7 @@ export async function upsertFilingTaxonomySnapshot(
derived_metrics: input.derived_metrics,
validation_result: input.validation_result,
normalization_summary: normalized.normalization_summary,
issuer_overlay_revision_id: input.issuer_overlay_revision_id ?? null,
facts_count: input.facts_count,
concepts_count: input.concepts_count,
dimensions_count: input.dimensions_count,
@@ -1579,7 +1599,7 @@ export async function listTaxonomyFactsByTicker(input: {
};
}
export async function listTaxonomyAssetsBySnapshotIds(snapshotIds: number[]) {
async function listTaxonomyAssetsBySnapshotIds(snapshotIds: number[]) {
if (snapshotIds.length === 0) {
return [];
}
@@ -1593,6 +1613,22 @@ export async function listTaxonomyAssetsBySnapshotIds(snapshotIds: number[]) {
return rows.map(toAssetRecord);
}
export async function listFilingTaxonomyConceptsBySnapshotIds(
snapshotIds: number[],
) {
if (snapshotIds.length === 0) {
return [];
}
const rows = await db
.select()
.from(filingTaxonomyConcept)
.where(inArray(filingTaxonomyConcept.snapshot_id, snapshotIds))
.orderBy(desc(filingTaxonomyConcept.id));
return rows.map(toConceptRecord);
}
export const __filingTaxonomyInternals = {
normalizeFilingTaxonomySnapshotPayload,
toSnapshotRecord,

View File

@@ -0,0 +1,146 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Database } from "bun:sqlite";
import { __dbInternals } from "@/lib/server/db";
import type {
IssuerOverlayDefinition,
IssuerOverlayDiagnostics,
IssuerOverlayStats,
} from "@/lib/server/db/schema";
let tempDir: string | null = null;
let sqliteClient: Database | null = null;
let overlayRepo: typeof import("./issuer-overlays") | null = null;
function resetDbSingletons() {
const globalState = globalThis as typeof globalThis & {
__fiscalSqliteClient?: Database;
__fiscalDrizzleDb?: unknown;
__financialIngestionSchemaStatus?: unknown;
};
globalState.__fiscalSqliteClient?.close();
globalState.__fiscalSqliteClient = undefined;
globalState.__fiscalDrizzleDb = undefined;
globalState.__financialIngestionSchemaStatus = undefined;
}
function sampleDefinition(): IssuerOverlayDefinition {
return {
version: "fiscal-v1",
ticker: "AAPL",
pack: "core",
mappings: [
{
surface_key: "revenue",
statement: "income",
allowed_source_concepts: ["aapl:ServicesRevenue", "aapl:ProductRevenue"],
allowed_authoritative_concepts: [],
},
],
};
}
function sampleDiagnostics(): IssuerOverlayDiagnostics {
return {
pack: "core",
sampledSnapshotIds: [11, 12],
acceptedMappings: [
{
qname: "aapl:ServicesRevenue",
surface_key: "revenue",
statement: "income",
reason: "local_name_match",
source_snapshot_ids: [11, 12],
},
],
rejectedMappings: [],
};
}
function sampleStats(): IssuerOverlayStats {
return {
pack: "core",
sampledSnapshotCount: 2,
sampledSnapshotIds: [11, 12],
acceptedMappingCount: 1,
rejectedMappingCount: 0,
publishedRevisionNumber: null,
};
}
describe("issuer overlay repo", () => {
beforeAll(async () => {
tempDir = mkdtempSync(join(tmpdir(), "fiscal-issuer-overlay-"));
const env = process.env as Record<string, string | undefined>;
env.DATABASE_URL = `file:${join(tempDir, "repo.sqlite")}`;
env.NODE_ENV = "test";
resetDbSingletons();
sqliteClient = new Database(join(tempDir, "repo.sqlite"), { create: true });
sqliteClient.exec("PRAGMA foreign_keys = ON;");
__dbInternals.ensureLocalSqliteSchema(sqliteClient);
const globalState = globalThis as typeof globalThis & {
__fiscalSqliteClient?: Database;
__fiscalDrizzleDb?: unknown;
};
globalState.__fiscalSqliteClient = sqliteClient;
globalState.__fiscalDrizzleDb = undefined;
overlayRepo = await import("./issuer-overlays");
});
afterAll(() => {
sqliteClient?.close();
resetDbSingletons();
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
}
});
beforeEach(() => {
sqliteClient?.exec("DELETE FROM issuer_overlay;");
sqliteClient?.exec("DELETE FROM issuer_overlay_revision;");
});
it("creates an empty overlay row on ensure", async () => {
if (!overlayRepo) {
throw new Error("overlay repo not initialized");
}
const overlay = await overlayRepo.ensureIssuerOverlayRow("aapl");
expect(overlay?.ticker).toBe("AAPL");
expect(overlay?.status).toBe("empty");
expect(overlay?.active_revision).toBeNull();
});
it("publishes and deduplicates overlay revisions by content hash", async () => {
if (!overlayRepo) {
throw new Error("overlay repo not initialized");
}
const first = await overlayRepo.publishIssuerOverlayRevision({
ticker: "AAPL",
definition: sampleDefinition(),
diagnostics: sampleDiagnostics(),
stats: sampleStats(),
});
const second = await overlayRepo.publishIssuerOverlayRevision({
ticker: "AAPL",
definition: sampleDefinition(),
diagnostics: sampleDiagnostics(),
stats: sampleStats(),
});
const overlay = await overlayRepo.getIssuerOverlay("AAPL");
const revisions = await overlayRepo.listIssuerOverlayRevisions("AAPL");
expect(first.published).toBe(true);
expect(second.published).toBe(false);
expect(overlay?.active_revision?.id).toBe(first.revision.id);
expect(revisions).toHaveLength(1);
});
});

View File

@@ -0,0 +1,331 @@
import { createHash } from "node:crypto";
import { and, desc, eq, max } from "drizzle-orm";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { getSqliteClient } from "@/lib/server/db";
import {
issuerOverlay,
issuerOverlayRevision,
schema,
type IssuerOverlayDefinition,
type IssuerOverlayDiagnostics,
type IssuerOverlayStats,
} from "@/lib/server/db/schema";
export type IssuerOverlayRevisionRecord = {
id: number;
ticker: string;
revision_number: number;
definition_hash: string;
definition_json: IssuerOverlayDefinition;
diagnostics_json: IssuerOverlayDiagnostics | null;
source_snapshot_ids: number[];
created_at: string;
};
export type IssuerOverlayRecord = {
ticker: string;
status: "empty" | "active" | "error";
active_revision_id: number | null;
last_built_at: string | null;
last_error: string | null;
stats_json: IssuerOverlayStats | null;
created_at: string;
updated_at: string;
active_revision: IssuerOverlayRevisionRecord | null;
};
function getDb() {
return drizzle(getSqliteClient(), { schema });
}
function normalizeTicker(ticker: string) {
return ticker.trim().toUpperCase();
}
function uniqueSorted(values: string[]) {
return [...new Set(values.filter((value) => value.trim().length > 0))].sort(
(left, right) => left.localeCompare(right),
);
}
export function normalizeIssuerOverlayDefinition(
input: IssuerOverlayDefinition,
): IssuerOverlayDefinition {
return {
version: "fiscal-v1",
ticker: normalizeTicker(input.ticker),
pack: input.pack?.trim() ? input.pack.trim() : null,
mappings: [...input.mappings]
.map((mapping) => ({
surface_key: mapping.surface_key,
statement: mapping.statement,
allowed_source_concepts: uniqueSorted(mapping.allowed_source_concepts),
allowed_authoritative_concepts: uniqueSorted(
mapping.allowed_authoritative_concepts,
),
}))
.filter(
(mapping) =>
mapping.allowed_source_concepts.length > 0 ||
mapping.allowed_authoritative_concepts.length > 0,
)
.sort((left, right) => {
return (
left.statement.localeCompare(right.statement) ||
left.surface_key.localeCompare(right.surface_key)
);
}),
};
}
function definitionHash(definition: IssuerOverlayDefinition) {
return createHash("sha256")
.update(JSON.stringify(normalizeIssuerOverlayDefinition(definition)))
.digest("hex");
}
function toRevisionRecord(
row: typeof issuerOverlayRevision.$inferSelect,
): IssuerOverlayRevisionRecord {
const definition = row.definition_json;
if (!definition) {
throw new Error(
`Issuer overlay revision ${row.id} is missing definition_json`,
);
}
return {
id: row.id,
ticker: row.ticker,
revision_number: row.revision_number,
definition_hash: row.definition_hash,
definition_json: normalizeIssuerOverlayDefinition(definition),
diagnostics_json: row.diagnostics_json ?? null,
source_snapshot_ids: row.source_snapshot_ids ?? [],
created_at: row.created_at,
};
}
async function getRevisionById(id: number | null) {
if (!id) {
return null;
}
const [row] = await getDb()
.select()
.from(issuerOverlayRevision)
.where(eq(issuerOverlayRevision.id, id))
.limit(1);
return row ? toRevisionRecord(row) : null;
}
export async function getIssuerOverlay(ticker: string) {
const normalizedTicker = normalizeTicker(ticker);
if (!normalizedTicker) {
return null;
}
const [row] = await getDb()
.select()
.from(issuerOverlay)
.where(eq(issuerOverlay.ticker, normalizedTicker))
.limit(1);
if (!row) {
return null;
}
return {
ticker: row.ticker,
status: row.status,
active_revision_id: row.active_revision_id ?? null,
last_built_at: row.last_built_at,
last_error: row.last_error,
stats_json: row.stats_json ?? null,
created_at: row.created_at,
updated_at: row.updated_at,
active_revision: await getRevisionById(row.active_revision_id ?? null),
} satisfies IssuerOverlayRecord;
}
export async function ensureIssuerOverlayRow(ticker: string) {
const normalizedTicker = normalizeTicker(ticker);
if (!normalizedTicker) {
return null;
}
const now = new Date().toISOString();
await getDb()
.insert(issuerOverlay)
.values({
ticker: normalizedTicker,
status: "empty",
active_revision_id: null,
last_built_at: null,
last_error: null,
stats_json: null,
created_at: now,
updated_at: now,
})
.onConflictDoNothing();
return await getIssuerOverlay(normalizedTicker);
}
export async function markIssuerOverlayBuildState(input: {
ticker: string;
status: "empty" | "active" | "error";
lastError?: string | null;
stats?: IssuerOverlayStats | null;
activeRevisionId?: number | null;
}) {
const normalizedTicker = normalizeTicker(input.ticker);
const now = new Date().toISOString();
await ensureIssuerOverlayRow(normalizedTicker);
await getDb()
.update(issuerOverlay)
.set({
status: input.status,
last_error: input.lastError ?? null,
last_built_at: now,
stats_json: input.stats ?? null,
active_revision_id:
input.activeRevisionId === undefined ? undefined : input.activeRevisionId,
updated_at: now,
})
.where(eq(issuerOverlay.ticker, normalizedTicker));
return await getIssuerOverlay(normalizedTicker);
}
export async function getActiveIssuerOverlayDefinition(ticker: string) {
const overlay = await getIssuerOverlay(ticker);
return overlay?.active_revision?.definition_json ?? null;
}
export async function publishIssuerOverlayRevision(input: {
ticker: string;
definition: IssuerOverlayDefinition;
diagnostics: IssuerOverlayDiagnostics;
stats: IssuerOverlayStats;
}) {
const normalizedTicker = normalizeTicker(input.ticker);
const normalizedDefinition = normalizeIssuerOverlayDefinition({
...input.definition,
ticker: normalizedTicker,
});
const hash = definitionHash(normalizedDefinition);
const now = new Date().toISOString();
return await getDb().transaction(async (tx) => {
const [currentOverlay] = await tx
.select()
.from(issuerOverlay)
.where(eq(issuerOverlay.ticker, normalizedTicker))
.limit(1);
if (!currentOverlay) {
await tx.insert(issuerOverlay).values({
ticker: normalizedTicker,
status: "empty",
active_revision_id: null,
last_built_at: now,
last_error: null,
stats_json: null,
created_at: now,
updated_at: now,
});
}
const [existingRevision] = await tx
.select()
.from(issuerOverlayRevision)
.where(
and(
eq(issuerOverlayRevision.ticker, normalizedTicker),
eq(issuerOverlayRevision.definition_hash, hash),
),
)
.limit(1);
const currentActiveRevisionId = currentOverlay?.active_revision_id ?? null;
if (existingRevision) {
await tx
.update(issuerOverlay)
.set({
status:
normalizedDefinition.mappings.length > 0 ? "active" : "empty",
active_revision_id:
normalizedDefinition.mappings.length > 0
? existingRevision.id
: currentActiveRevisionId,
last_built_at: now,
last_error: null,
stats_json: input.stats,
updated_at: now,
})
.where(eq(issuerOverlay.ticker, normalizedTicker));
return {
published:
normalizedDefinition.mappings.length > 0 &&
currentActiveRevisionId !== existingRevision.id,
revision: toRevisionRecord(existingRevision),
};
}
const [currentRevisionNumberRow] = await tx
.select({
value: max(issuerOverlayRevision.revision_number),
})
.from(issuerOverlayRevision)
.where(eq(issuerOverlayRevision.ticker, normalizedTicker));
const nextRevisionNumber = (currentRevisionNumberRow?.value ?? 0) + 1;
const [savedRevision] = await tx
.insert(issuerOverlayRevision)
.values({
ticker: normalizedTicker,
revision_number: nextRevisionNumber,
definition_hash: hash,
definition_json: normalizedDefinition,
diagnostics_json: input.diagnostics,
source_snapshot_ids: input.diagnostics.sampledSnapshotIds,
created_at: now,
})
.returning();
await tx
.update(issuerOverlay)
.set({
status: normalizedDefinition.mappings.length > 0 ? "active" : "empty",
active_revision_id:
normalizedDefinition.mappings.length > 0
? savedRevision.id
: currentActiveRevisionId,
last_built_at: now,
last_error: null,
stats_json: input.stats,
updated_at: now,
})
.where(eq(issuerOverlay.ticker, normalizedTicker));
return {
published: normalizedDefinition.mappings.length > 0,
revision: toRevisionRecord(savedRevision),
};
});
}
export async function listIssuerOverlayRevisions(ticker: string) {
const normalizedTicker = normalizeTicker(ticker);
const rows = await getDb()
.select()
.from(issuerOverlayRevision)
.where(eq(issuerOverlayRevision.ticker, normalizedTicker))
.orderBy(desc(issuerOverlayRevision.revision_number));
return rows.map(toRevisionRecord);
}

View File

@@ -457,7 +457,7 @@ export async function createResearchArtifactRecord(input: {
return toResearchArtifact(created);
}
export async function upsertSystemResearchArtifact(input: {
async function upsertSystemResearchArtifact(input: {
userId: string;
organizationId?: string | null;
ticker: string;
@@ -837,7 +837,7 @@ export async function deleteResearchMemoEvidenceLink(userId: string, memoId: num
return rows.length > 0;
}
export async function listResearchMemoEvidenceLinks(userId: string, ticker: string): Promise<ResearchMemoEvidenceLink[]> {
async function listResearchMemoEvidenceLinks(userId: string, ticker: string): Promise<ResearchMemoEvidenceLink[]> {
const memo = await getResearchMemoByTicker(userId, ticker);
if (!memo) {
return [];
@@ -1116,7 +1116,7 @@ export async function getResearchArtifactFileResponse(userId: string, id: number
});
}
export function rebuildResearchArtifactIndex() {
function rebuildResearchArtifactIndex() {
rebuildArtifactSearchIndex();
}

View File

@@ -153,7 +153,7 @@ export async function createTaskRunRecord(input: CreateTaskInput) {
});
}
export type AtomicCreateResult =
type AtomicCreateResult =
| { task: Task; created: true }
| { task: null; created: false };