Remove legacy TypeScript financial surface mapping, make Rust JSON single source of truth

- Delete standard-template.ts, surface.ts, materialize.ts (dead code)
- Delete financial-taxonomy.test.ts (relied on removed code)
- Add missing income statement surfaces to core.surface.json
- Add cost_of_revenue mapping to core.income-bridge.json
- Refactor standardize.ts to remove template dependency
- Simplify financial-taxonomy.ts to use only DB snapshots
- Add architecture documentation
This commit is contained in:
2026-03-15 14:38:48 -04:00
parent 7a42d73a48
commit a7f7be50b4
9 changed files with 574 additions and 5009 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,11 @@
import type {
DerivedFinancialRow,
DimensionBreakdownRow,
FinancialStatementKind,
FinancialStatementPeriod,
FinancialUnit,
StandardizedFinancialRow,
TaxonomyFactRow,
TaxonomyStatementRow
} from '@/lib/types';
import {
STANDARD_FINANCIAL_TEMPLATES,
type StandardTemplateRowDefinition,
type TemplateFormula
} from '@/lib/server/financials/standard-template';
function normalizeToken(value: string) {
return value.trim().toLowerCase();
}
function tokenizeLabel(value: string) {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, ' ')
.trim()
.split(/\s+/)
.filter((token) => token.length > 0);
}
function valueOrNull(values: Record<string, number | null>, periodId: string) {
return periodId in values ? values[periodId] : null;
@@ -39,326 +19,6 @@ function sumValues(values: Array<number | null>, treatNullAsZero = false) {
return values.reduce<number>((sum, value) => sum + (value ?? 0), 0);
}
function subtractValues(left: number | null, right: number | null) {
if (left === null || right === null) {
return null;
}
return left - right;
}
function divideValues(left: number | null, right: number | null) {
if (left === null || right === null || right === 0) {
return null;
}
return left / right;
}
type CandidateMatchKind = 'exact_local_name' | 'secondary_local_name' | 'label_phrase';
type StatementRowCandidate = {
row: TaxonomyStatementRow;
matchKind: CandidateMatchKind;
aliasRank: number;
unit: FinancialUnit;
labelTokenCount: number;
matchedPhraseTokenCount: number;
};
type FactCandidate = {
fact: TaxonomyFactRow;
matchKind: Exclude<CandidateMatchKind, 'label_phrase'>;
aliasRank: number;
unit: FinancialUnit;
};
type ResolvedCandidate =
| {
sourceType: 'row';
matchKind: CandidateMatchKind;
aliasRank: number;
unit: FinancialUnit;
labelTokenCount: number;
matchedPhraseTokenCount: number;
row: TaxonomyStatementRow;
}
| {
sourceType: 'fact';
matchKind: Exclude<CandidateMatchKind, 'label_phrase'>;
aliasRank: number;
unit: FinancialUnit;
fact: TaxonomyFactRow;
};
type DerivedRole = 'expense' | 'addback';
type InternalRowMetadata = {
derivedRoleByPeriod: Record<string, DerivedRole | null>;
};
function resolvedCandidatesForPeriod(input: {
definition: StandardTemplateRowDefinition;
candidates: StatementRowCandidate[];
factCandidates: FactCandidate[];
period: FinancialStatementPeriod;
}) {
const rowCandidates = input.candidates
.filter((candidate) => input.period.id in candidate.row.values && candidate.row.values[input.period.id] !== null)
.map((candidate) => ({
sourceType: 'row' as const,
...candidate
}));
const factCandidates = input.factCandidates
.filter((candidate) => factMatchesPeriod(candidate.fact, input.period))
.map((candidate) => ({
sourceType: 'fact' as const,
...candidate
}));
if (input.definition.selectionPolicy === 'aggregate_multiple_components') {
const aggregateCandidates = [...rowCandidates, ...factCandidates]
.sort((left, right) => compareResolvedCandidates(left, right, input.definition));
const dedupedCandidates: ResolvedCandidate[] = [];
const seenConcepts = new Set<string>();
for (const candidate of aggregateCandidates) {
const conceptKey = candidate.sourceType === 'row'
? candidate.row.key
: candidate.fact.conceptKey;
if (seenConcepts.has(conceptKey)) {
continue;
}
seenConcepts.add(conceptKey);
dedupedCandidates.push(candidate);
}
return dedupedCandidates;
}
const resolvedCandidate = [...rowCandidates, ...factCandidates]
.sort((left, right) => compareResolvedCandidates(left, right, input.definition))[0];
return resolvedCandidate ? [resolvedCandidate] : [];
}
const GLOBAL_EXCLUDE_LABEL_PHRASES = [
'pro forma',
'reconciliation',
'acquiree',
'business combination',
'assets acquired',
'liabilities assumed'
] as const;
function inferUnit(rawUnit: string | null, fallback: FinancialUnit) {
const normalized = (rawUnit ?? '').toLowerCase();
if (!normalized) {
return fallback;
}
if (normalized.includes('usd') || normalized.includes('iso4217')) {
return 'currency';
}
if (normalized.includes('shares')) {
return 'shares';
}
if (normalized.includes('pure') || normalized.includes('percent')) {
return fallback === 'percent' ? 'percent' : 'ratio';
}
return fallback;
}
function rowUnit(row: TaxonomyStatementRow, fallback: FinancialUnit) {
return inferUnit(Object.values(row.units)[0] ?? null, fallback);
}
function isUnitCompatible(expected: FinancialUnit, actual: FinancialUnit) {
if (expected === actual) {
return true;
}
if ((expected === 'percent' || expected === 'ratio') && (actual === 'percent' || actual === 'ratio')) {
return true;
}
return false;
}
function phraseTokens(phrase: string) {
return tokenizeLabel(phrase);
}
function labelContainsPhrase(labelTokens: string[], phrase: string) {
const target = phraseTokens(phrase);
if (target.length === 0 || target.length > labelTokens.length) {
return false;
}
for (let index = 0; index <= labelTokens.length - target.length; index += 1) {
let matched = true;
for (let offset = 0; offset < target.length; offset += 1) {
if (labelTokens[index + offset] !== target[offset]) {
matched = false;
break;
}
}
if (matched) {
return true;
}
}
return false;
}
function matchRank(matchKind: CandidateMatchKind) {
switch (matchKind) {
case 'exact_local_name':
return 0;
case 'secondary_local_name':
return 1;
case 'label_phrase':
return 2;
}
}
function aliasRank(localName: string, aliases: readonly string[] | undefined) {
const normalizedLocalName = normalizeToken(localName);
const matchIndex = (aliases ?? []).findIndex((alias) => normalizeToken(alias) === normalizedLocalName);
return matchIndex === -1 ? Number.MAX_SAFE_INTEGER : matchIndex;
}
function applySignTransform(value: number | null, transform: StandardTemplateRowDefinition['signTransform']) {
if (value === null || !transform) {
return value;
}
if (transform === 'invert') {
return value * -1;
}
return Math.abs(value);
}
function classifyStatementRowCandidate(
row: TaxonomyStatementRow,
definition: StandardTemplateRowDefinition
) {
if (definition.selectionPolicy === 'formula_only') {
return null;
}
const rowLocalName = normalizeToken(row.localName);
if ((definition.matchers.excludeLocalNames ?? []).some((localName) => normalizeToken(localName) === rowLocalName)) {
return null;
}
const labelTokens = tokenizeLabel(row.label);
const excludedLabelPhrases = [
...GLOBAL_EXCLUDE_LABEL_PHRASES,
...(definition.matchers.excludeLabelPhrases ?? [])
];
if (excludedLabelPhrases.some((phrase) => labelContainsPhrase(labelTokens, phrase))) {
return null;
}
const unit = rowUnit(row, definition.unit);
if (!isUnitCompatible(definition.unit, unit)) {
return null;
}
if ((definition.matchers.exactLocalNames ?? []).some((localName) => normalizeToken(localName) === rowLocalName)) {
return {
row,
matchKind: 'exact_local_name',
aliasRank: aliasRank(row.localName, definition.matchers.exactLocalNames),
unit,
labelTokenCount: labelTokens.length,
matchedPhraseTokenCount: 0
} satisfies StatementRowCandidate;
}
if ((definition.matchers.secondaryLocalNames ?? []).some((localName) => normalizeToken(localName) === rowLocalName)) {
return {
row,
matchKind: 'secondary_local_name',
aliasRank: aliasRank(row.localName, definition.matchers.secondaryLocalNames),
unit,
labelTokenCount: labelTokens.length,
matchedPhraseTokenCount: 0
} satisfies StatementRowCandidate;
}
const matchedPhrase = (definition.matchers.allowedLabelPhrases ?? [])
.map((phrase) => ({
phrase,
tokenCount: phraseTokens(phrase).length
}))
.filter(({ phrase }) => labelContainsPhrase(labelTokens, phrase))
.sort((left, right) => right.tokenCount - left.tokenCount)[0];
if (!matchedPhrase) {
return null;
}
if (row.hasDimensions) {
return null;
}
return {
row,
matchKind: 'label_phrase',
aliasRank: Number.MAX_SAFE_INTEGER,
unit,
labelTokenCount: labelTokens.length,
matchedPhraseTokenCount: matchedPhrase.tokenCount
} satisfies StatementRowCandidate;
}
function classifyFactCandidate(
fact: TaxonomyFactRow,
definition: StandardTemplateRowDefinition
) {
if (!fact.isDimensionless) {
return null;
}
const localName = normalizeToken(fact.localName);
if ((definition.matchers.excludeLocalNames ?? []).some((entry) => normalizeToken(entry) === localName)) {
return null;
}
const unit = inferUnit(fact.unit ?? null, definition.unit);
if (!isUnitCompatible(definition.unit, unit)) {
return null;
}
if ((definition.matchers.exactLocalNames ?? []).some((entry) => normalizeToken(entry) === localName)) {
return {
fact,
matchKind: 'exact_local_name',
aliasRank: aliasRank(fact.localName, definition.matchers.exactLocalNames),
unit
} satisfies FactCandidate;
}
if ((definition.matchers.secondaryLocalNames ?? []).some((entry) => normalizeToken(entry) === localName)) {
return {
fact,
matchKind: 'secondary_local_name',
aliasRank: aliasRank(fact.localName, definition.matchers.secondaryLocalNames),
unit
} satisfies FactCandidate;
}
return null;
}
export function factMatchesPeriod(fact: TaxonomyFactRow, period: FinancialStatementPeriod) {
if (period.periodStart) {
return fact.periodStart === period.periodStart && fact.periodEnd === period.periodEnd;
@@ -367,390 +27,6 @@ export function factMatchesPeriod(fact: TaxonomyFactRow, period: FinancialStatem
return (fact.periodInstant ?? fact.periodEnd) === period.periodEnd;
}
function compareStatementRowCandidates(
left: StatementRowCandidate,
right: StatementRowCandidate,
definition: StandardTemplateRowDefinition
) {
const matchDelta = matchRank(left.matchKind) - matchRank(right.matchKind);
if (matchDelta !== 0) {
return matchDelta;
}
if (left.aliasRank !== right.aliasRank) {
return left.aliasRank - right.aliasRank;
}
if (left.row.hasDimensions !== right.row.hasDimensions) {
return left.row.hasDimensions ? 1 : -1;
}
if (definition.selectionPolicy === 'prefer_primary_statement_concept' && left.row.isExtension !== right.row.isExtension) {
return left.row.isExtension ? 1 : -1;
}
if (left.row.order !== right.row.order) {
return left.row.order - right.row.order;
}
if (left.matchedPhraseTokenCount !== right.matchedPhraseTokenCount) {
return right.matchedPhraseTokenCount - left.matchedPhraseTokenCount;
}
if (left.labelTokenCount !== right.labelTokenCount) {
return left.labelTokenCount - right.labelTokenCount;
}
return left.row.label.localeCompare(right.row.label);
}
function compareFactCandidates(left: FactCandidate, right: FactCandidate) {
const matchDelta = matchRank(left.matchKind) - matchRank(right.matchKind);
if (matchDelta !== 0) {
return matchDelta;
}
if (left.aliasRank !== right.aliasRank) {
return left.aliasRank - right.aliasRank;
}
return left.fact.qname.localeCompare(right.fact.qname);
}
function compareResolvedCandidates(
left: ResolvedCandidate,
right: ResolvedCandidate,
definition: StandardTemplateRowDefinition
) {
const matchDelta = matchRank(left.matchKind) - matchRank(right.matchKind);
if (matchDelta !== 0) {
return matchDelta;
}
if (left.aliasRank !== right.aliasRank) {
return left.aliasRank - right.aliasRank;
}
if (left.sourceType === 'row' && right.sourceType === 'row') {
return compareStatementRowCandidates(left, right, definition);
}
if (left.sourceType === 'fact' && right.sourceType === 'fact') {
return compareFactCandidates(left, right);
}
if (left.sourceType === 'row' && right.sourceType === 'fact') {
return left.row.hasDimensions ? 1 : -1;
}
if (left.sourceType === 'fact' && right.sourceType === 'row') {
return right.row.hasDimensions ? -1 : 1;
}
return 0;
}
function buildTemplateRow(
definition: StandardTemplateRowDefinition,
candidates: StatementRowCandidate[],
factCandidates: FactCandidate[],
periods: FinancialStatementPeriod[]
) {
const sourceConcepts = new Set<string>();
const sourceRowKeys = new Set<string>();
const sourceFactIds = new Set<number>();
const matchedRowKeys = new Set<string>();
const values: Record<string, number | null> = Object.fromEntries(periods.map((period) => [period.id, null]));
const resolvedSourceRowKeys: Record<string, string | null> = Object.fromEntries(periods.map((period) => [period.id, null]));
const metadata: InternalRowMetadata = {
derivedRoleByPeriod: Object.fromEntries(periods.map((period) => [period.id, null]))
};
let unit = definition.unit;
let hasDimensions = false;
for (const period of periods) {
const resolvedCandidates = resolvedCandidatesForPeriod({
definition,
candidates,
factCandidates,
period
});
if (resolvedCandidates.length === 0) {
continue;
}
if (definition.key === 'depreciation_and_amortization') {
metadata.derivedRoleByPeriod[period.id] = resolvedCandidates.some((candidate) => {
const localName = candidate.sourceType === 'row'
? candidate.row.localName
: candidate.fact.localName;
return normalizeToken(localName) === normalizeToken('CostOfGoodsAndServicesSoldDepreciationAndAmortization');
})
? 'expense'
: 'addback';
}
values[period.id] = definition.selectionPolicy === 'aggregate_multiple_components'
? sumValues(resolvedCandidates.map((candidate) => {
if (candidate.sourceType === 'row') {
return applySignTransform(candidate.row.values[period.id] ?? null, definition.signTransform);
}
return applySignTransform(candidate.fact.value ?? null, definition.signTransform);
}))
: (() => {
const resolvedCandidate = resolvedCandidates[0]!;
if (resolvedCandidate.sourceType === 'row') {
return applySignTransform(resolvedCandidate.row.values[period.id] ?? null, definition.signTransform);
}
return applySignTransform(resolvedCandidate.fact.value ?? null, definition.signTransform);
})();
resolvedSourceRowKeys[period.id] = resolvedCandidates.length === 1
? (resolvedCandidates[0]!.sourceType === 'row'
? resolvedCandidates[0]!.row.key
: resolvedCandidates[0]!.fact.conceptKey ?? null)
: null;
for (const resolvedCandidate of resolvedCandidates) {
unit = resolvedCandidate.unit;
if (resolvedCandidate.sourceType === 'row') {
hasDimensions = hasDimensions || resolvedCandidate.row.hasDimensions;
matchedRowKeys.add(resolvedCandidate.row.key);
sourceConcepts.add(resolvedCandidate.row.qname);
sourceRowKeys.add(resolvedCandidate.row.key);
for (const factId of resolvedCandidate.row.sourceFactIds) {
sourceFactIds.add(factId);
}
continue;
}
sourceConcepts.add(resolvedCandidate.fact.qname);
sourceRowKeys.add(resolvedCandidate.fact.conceptKey);
sourceFactIds.add(resolvedCandidate.fact.id);
}
}
return {
row: {
key: definition.key,
label: definition.label,
category: definition.category,
templateSection: definition.category,
order: definition.order,
unit,
values,
sourceConcepts: [...sourceConcepts].sort((left, right) => left.localeCompare(right)),
sourceRowKeys: [...sourceRowKeys].sort((left, right) => left.localeCompare(right)),
sourceFactIds: [...sourceFactIds].sort((left, right) => left - right),
formulaKey: null,
hasDimensions,
resolvedSourceRowKeys
} satisfies StandardizedFinancialRow,
matchedRowKeys,
metadata
};
}
function computeFormulaValue(
formula: TemplateFormula,
rowsByKey: Map<string, StandardizedFinancialRow>,
periodId: string
) {
switch (formula.kind) {
case 'sum':
return sumValues(
formula.sourceKeys.map((key) => valueOrNull(rowsByKey.get(key)?.values ?? {}, periodId)),
formula.treatNullAsZero ?? false
);
case 'subtract':
return subtractValues(
valueOrNull(rowsByKey.get(formula.left)?.values ?? {}, periodId),
valueOrNull(rowsByKey.get(formula.right)?.values ?? {}, periodId)
);
case 'divide':
return divideValues(
valueOrNull(rowsByKey.get(formula.numerator)?.values ?? {}, periodId),
valueOrNull(rowsByKey.get(formula.denominator)?.values ?? {}, periodId)
);
}
}
function rowValueForPeriod(
rowsByKey: Map<string, StandardizedFinancialRow>,
key: string,
periodId: string
) {
return valueOrNull(rowsByKey.get(key)?.values ?? {}, periodId);
}
function computeOperatingIncomeFallbackValue(
rowsByKey: Map<string, StandardizedFinancialRow>,
rowMetadataByKey: Map<string, InternalRowMetadata>,
periodId: string
) {
const grossProfit = rowValueForPeriod(rowsByKey, 'gross_profit', periodId);
const sellingGeneralAndAdministrative = rowValueForPeriod(rowsByKey, 'selling_general_and_administrative', periodId);
const researchAndDevelopment = rowValueForPeriod(rowsByKey, 'research_and_development', periodId) ?? 0;
const depreciationAndAmortization = rowValueForPeriod(rowsByKey, 'depreciation_and_amortization', periodId);
const depreciationRole = rowMetadataByKey.get('depreciation_and_amortization')?.derivedRoleByPeriod[periodId] ?? null;
if (
depreciationRole === 'expense'
&& grossProfit !== null
&& sellingGeneralAndAdministrative !== null
&& depreciationAndAmortization !== null
) {
return grossProfit - sellingGeneralAndAdministrative - researchAndDevelopment - depreciationAndAmortization;
}
const pretaxIncome = rowValueForPeriod(rowsByKey, 'pretax_income', periodId);
if (pretaxIncome === null) {
return null;
}
const interestExpense = rowValueForPeriod(rowsByKey, 'interest_expense', periodId) ?? 0;
const interestIncome = rowValueForPeriod(rowsByKey, 'interest_income', periodId) ?? 0;
const otherNonOperatingIncome = rowValueForPeriod(rowsByKey, 'other_non_operating_income', periodId) ?? 0;
return pretaxIncome + interestExpense - interestIncome - otherNonOperatingIncome;
}
function computeFallbackValueForDefinition(
definition: StandardTemplateRowDefinition,
rowsByKey: Map<string, StandardizedFinancialRow>,
rowMetadataByKey: Map<string, InternalRowMetadata>,
periodId: string
) {
if (definition.key === 'operating_income') {
return computeOperatingIncomeFallbackValue(rowsByKey, rowMetadataByKey, periodId);
}
if (!definition.fallbackFormula) {
return null;
}
return computeFormulaValue(definition.fallbackFormula, rowsByKey, periodId);
}
function applyFormulas(
rowsByKey: Map<string, StandardizedFinancialRow>,
rowMetadataByKey: Map<string, InternalRowMetadata>,
definitions: StandardTemplateRowDefinition[],
periods: FinancialStatementPeriod[]
) {
for (let pass = 0; pass < definitions.length; pass += 1) {
let changed = false;
for (const definition of definitions) {
if (!definition.fallbackFormula && definition.key !== 'operating_income') {
continue;
}
const target = rowsByKey.get(definition.key);
if (!target) {
continue;
}
let usedFormula = target.formulaKey !== null;
for (const period of periods) {
if (definition.selectionPolicy !== 'formula_only' && target.values[period.id] !== null) {
continue;
}
const computed = computeFallbackValueForDefinition(definition, rowsByKey, rowMetadataByKey, period.id);
if (computed === null) {
continue;
}
target.values[period.id] = applySignTransform(computed, definition.signTransform);
target.resolvedSourceRowKeys[period.id] = null;
usedFormula = true;
changed = true;
}
if (usedFormula) {
target.formulaKey = definition.key;
}
}
if (!changed) {
break;
}
}
}
export function buildStandardizedRows(input: {
rows: TaxonomyStatementRow[];
statement: Extract<FinancialStatementKind, 'income' | 'balance' | 'cash_flow'>;
periods: FinancialStatementPeriod[];
facts: TaxonomyFactRow[];
}) {
const definitions = STANDARD_FINANCIAL_TEMPLATES[input.statement];
const rowsByKey = new Map<string, StandardizedFinancialRow>();
const rowMetadataByKey = new Map<string, InternalRowMetadata>();
const matchedRowKeys = new Set<string>();
for (const definition of definitions) {
const candidates = input.rows
.map((row) => classifyStatementRowCandidate(row, definition))
.filter((candidate): candidate is StatementRowCandidate => candidate !== null);
const factCandidates = input.facts
.map((fact) => classifyFactCandidate(fact, definition))
.filter((candidate): candidate is FactCandidate => candidate !== null);
const templateRow = buildTemplateRow(definition, candidates, factCandidates, input.periods);
for (const rowKey of templateRow.matchedRowKeys) {
matchedRowKeys.add(rowKey);
}
const hasAnyValue = Object.values(templateRow.row.values).some((value) => value !== null);
if (hasAnyValue || definition.fallbackFormula || definition.key === 'operating_income') {
rowsByKey.set(definition.key, templateRow.row);
rowMetadataByKey.set(definition.key, templateRow.metadata);
}
}
applyFormulas(rowsByKey, rowMetadataByKey, definitions, input.periods);
const templateRows = definitions
.filter((definition) => definition.includeInOutput !== false)
.map((definition) => rowsByKey.get(definition.key))
.filter((row): row is StandardizedFinancialRow => row !== undefined);
const coveredTemplateSourceRowKeys = new Set(templateRows.flatMap((row) => row.sourceRowKeys));
const unmatchedRows = input.rows
.filter((row) => !matchedRowKeys.has(row.key))
.filter((row) => !(row.hasDimensions && coveredTemplateSourceRowKeys.has(row.key)))
.map((row) => ({
key: `other:${row.key}`,
label: row.label,
category: 'other',
templateSection: 'other',
order: 10_000 + row.order,
unit: inferUnit(Object.values(row.units)[0] ?? null, 'currency'),
values: { ...row.values },
sourceConcepts: [row.qname],
sourceRowKeys: [row.key],
sourceFactIds: [...row.sourceFactIds],
formulaKey: null,
hasDimensions: row.hasDimensions,
resolvedSourceRowKeys: Object.fromEntries(
input.periods.map((period) => [period.id, period.id in row.values ? row.key : null])
)
} satisfies StandardizedFinancialRow));
return [...templateRows, ...unmatchedRows].sort((left, right) => {
if (left.order !== right.order) {
return left.order - right.order;
}
return left.label.localeCompare(right.label);
});
}
export function buildDimensionBreakdown(
facts: TaxonomyFactRow[],
periods: FinancialStatementPeriod[],

View File

@@ -1,320 +0,0 @@
import type {
DetailFinancialRow,
FinancialStatementKind,
FinancialStatementPeriod,
NormalizationSummary,
StructuredKpiRow,
SurfaceDetailMap,
SurfaceFinancialRow,
TaxonomyFactRow,
TaxonomyStatementRow
} from '@/lib/types';
import { buildStandardizedRows } from '@/lib/server/financials/standardize';
type CompactStatement = Extract<FinancialStatementKind, 'income' | 'balance' | 'cash_flow'>;
type SurfaceDefinition = {
key: string;
label: string;
category: string;
order: number;
unit: SurfaceFinancialRow['unit'];
rowKey?: string;
componentKeys?: string[];
formula?: {
kind: 'subtract';
left: string;
right: string;
};
};
const EMPTY_SURFACE_ROWS: Record<FinancialStatementKind, SurfaceFinancialRow[]> = {
income: [],
balance: [],
cash_flow: [],
equity: [],
comprehensive_income: []
};
const EMPTY_DETAIL_ROWS: Record<FinancialStatementKind, SurfaceDetailMap> = {
income: {},
balance: {},
cash_flow: {},
equity: {},
comprehensive_income: {}
};
const SURFACE_DEFINITIONS: Record<CompactStatement, SurfaceDefinition[]> = {
income: [
{ key: 'revenue', label: 'Revenue', category: 'surface', order: 10, unit: 'currency', rowKey: 'revenue' },
{ key: 'cost_of_revenue', label: 'Cost of Revenue', category: 'surface', order: 20, unit: 'currency', rowKey: 'cost_of_revenue' },
{ key: 'gross_profit', label: 'Gross Profit', category: 'surface', order: 30, unit: 'currency', rowKey: 'gross_profit' },
{
key: 'operating_expenses',
label: 'Operating Expenses',
category: 'surface',
order: 40,
unit: 'currency',
componentKeys: ['selling_general_and_administrative', 'research_and_development', 'depreciation_and_amortization']
},
{ key: 'operating_income', label: 'Operating Income', category: 'surface', order: 50, unit: 'currency', rowKey: 'operating_income' },
{
key: 'interest_and_other',
label: 'Interest and Other',
category: 'surface',
order: 60,
unit: 'currency',
formula: {
kind: 'subtract',
left: 'pretax_income',
right: 'operating_income'
}
},
{ key: 'pretax_income', label: 'Pretax Income', category: 'surface', order: 70, unit: 'currency', rowKey: 'pretax_income' },
{ key: 'income_taxes', label: 'Income Taxes', category: 'surface', order: 80, unit: 'currency', rowKey: 'income_tax_expense' },
{ key: 'net_income', label: 'Net Income', category: 'surface', order: 90, unit: 'currency', rowKey: 'net_income' }
],
balance: [
{ key: 'cash_and_equivalents', label: 'Cash and Equivalents', category: 'surface', order: 10, unit: 'currency', rowKey: 'cash_and_equivalents' },
{ key: 'receivables', label: 'Receivables', category: 'surface', order: 20, unit: 'currency', rowKey: 'accounts_receivable' },
{ key: 'inventory', label: 'Inventory', category: 'surface', order: 30, unit: 'currency', rowKey: 'inventory' },
{ key: 'current_assets', label: 'Current Assets', category: 'surface', order: 40, unit: 'currency', rowKey: 'current_assets' },
{ key: 'ppe', label: 'Property, Plant & Equipment', category: 'surface', order: 50, unit: 'currency', rowKey: 'property_plant_equipment' },
{
key: 'goodwill_and_intangibles',
label: 'Goodwill and Intangibles',
category: 'surface',
order: 60,
unit: 'currency',
componentKeys: ['goodwill', 'intangible_assets']
},
{ key: 'total_assets', label: 'Total Assets', category: 'surface', order: 70, unit: 'currency', rowKey: 'total_assets' },
{ key: 'current_liabilities', label: 'Current Liabilities', category: 'surface', order: 80, unit: 'currency', rowKey: 'current_liabilities' },
{ key: 'debt', label: 'Debt', category: 'surface', order: 90, unit: 'currency', rowKey: 'total_debt' },
{ key: 'total_liabilities', label: 'Total Liabilities', category: 'surface', order: 100, unit: 'currency', rowKey: 'total_liabilities' },
{ key: 'shareholders_equity', label: 'Shareholders Equity', category: 'surface', order: 110, unit: 'currency', rowKey: 'total_equity' }
],
cash_flow: [
{ key: 'operating_cash_flow', label: 'Operating Cash Flow', category: 'surface', order: 10, unit: 'currency', rowKey: 'operating_cash_flow' },
{ key: 'capital_expenditures', label: 'Capital Expenditures', category: 'surface', order: 20, unit: 'currency', rowKey: 'capital_expenditures' },
{ key: 'acquisitions', label: 'Acquisitions', category: 'surface', order: 30, unit: 'currency', rowKey: 'acquisitions' },
{ key: 'investing_cash_flow', label: 'Investing Cash Flow', category: 'surface', order: 40, unit: 'currency', rowKey: 'investing_cash_flow' },
{ key: 'financing_cash_flow', label: 'Financing Cash Flow', category: 'surface', order: 50, unit: 'currency', rowKey: 'financing_cash_flow' },
{ key: 'free_cash_flow', label: 'Free Cash Flow', category: 'surface', order: 60, unit: 'currency', rowKey: 'free_cash_flow' }
]
};
function rowHasAnyValue(row: { values: Record<string, number | null> }) {
return Object.values(row.values).some((value) => value !== null);
}
function sumValues(values: Array<number | null>) {
if (values.every((value) => value === null)) {
return null;
}
return values.reduce<number>((sum, value) => sum + (value ?? 0), 0);
}
function valueForPeriod(
rowByKey: Map<string, SurfaceFinancialRow>,
rowKey: string,
periodId: string
) {
return rowByKey.get(rowKey)?.values[periodId] ?? null;
}
function maxAbsValue(values: Record<string, number | null>) {
return Object.values(values).reduce<number>((max, value) => Math.max(max, Math.abs(value ?? 0)), 0);
}
function detailUnit(row: SurfaceFinancialRow, faithfulRow: TaxonomyStatementRow | undefined) {
if (faithfulRow) {
return Object.values(faithfulRow.units)[0] ?? null;
}
switch (row.unit) {
case 'currency':
return 'USD';
case 'shares':
return 'shares';
case 'percent':
return 'pure';
default:
return null;
}
}
function buildDetailRow(input: {
row: SurfaceFinancialRow;
parentSurfaceKey: string;
faithfulRowByKey: Map<string, TaxonomyStatementRow>;
}): DetailFinancialRow {
const sourceRowKey = input.row.sourceRowKeys.find((key) => input.faithfulRowByKey.has(key)) ?? input.row.sourceRowKeys[0] ?? input.row.key;
const faithfulRow = sourceRowKey ? input.faithfulRowByKey.get(sourceRowKey) : undefined;
const qname = faithfulRow?.qname ?? input.row.sourceConcepts[0] ?? input.row.key;
const [prefix, ...rest] = qname.split(':');
const localName = faithfulRow?.localName ?? (rest.length > 0 ? rest.join(':') : qname);
return {
key: input.row.key,
parentSurfaceKey: input.parentSurfaceKey,
label: input.row.label,
conceptKey: faithfulRow?.conceptKey ?? sourceRowKey,
qname,
namespaceUri: faithfulRow?.namespaceUri ?? (prefix && rest.length > 0 ? `urn:unknown:${prefix}` : 'urn:surface'),
localName,
unit: detailUnit(input.row, faithfulRow),
values: { ...input.row.values },
sourceFactIds: [...input.row.sourceFactIds],
isExtension: faithfulRow?.isExtension ?? false,
dimensionsSummary: faithfulRow?.hasDimensions ? ['has_dimensions'] : [],
residualFlag: input.parentSurfaceKey === 'unmapped'
};
}
function baselineForStatement(statement: CompactStatement, rowByKey: Map<string, SurfaceFinancialRow>) {
const anchorKey = statement === 'balance' ? 'total_assets' : 'revenue';
return maxAbsValue(rowByKey.get(anchorKey)?.values ?? {});
}
function materialityThreshold(statement: CompactStatement, baseline: number) {
if (statement === 'balance') {
return Math.max(5_000_000, baseline * 0.005);
}
return Math.max(1_000_000, baseline * 0.01);
}
export function buildCompactHydrationModel(input: {
periods: FinancialStatementPeriod[];
faithfulRows: Record<FinancialStatementKind, TaxonomyStatementRow[]>;
facts: TaxonomyFactRow[];
kpiRows?: StructuredKpiRow[];
}) {
const surfaceRows = structuredClone(EMPTY_SURFACE_ROWS);
const detailRows = structuredClone(EMPTY_DETAIL_ROWS);
let surfaceRowCount = 0;
let detailRowCount = 0;
let unmappedRowCount = 0;
let materialUnmappedRowCount = 0;
for (const statement of Object.keys(SURFACE_DEFINITIONS) as CompactStatement[]) {
const faithfulRows = input.faithfulRows[statement] ?? [];
const facts = input.facts.filter((fact) => fact.statement === statement);
const fullRows = buildStandardizedRows({
rows: faithfulRows,
statement,
periods: input.periods,
facts
});
const rowByKey = new Map(fullRows.map((row) => [row.key, row]));
const faithfulRowByKey = new Map(faithfulRows.map((row) => [row.key, row]));
const statementDetails: SurfaceDetailMap = {};
for (const definition of SURFACE_DEFINITIONS[statement]) {
const contributingRows = definition.rowKey
? [rowByKey.get(definition.rowKey)].filter((row): row is SurfaceFinancialRow => row !== undefined)
: (definition.componentKeys ?? [])
.map((key) => rowByKey.get(key))
.filter((row): row is SurfaceFinancialRow => row !== undefined);
const values = Object.fromEntries(input.periods.map((period) => {
const nextValue = definition.rowKey
? valueForPeriod(rowByKey, definition.rowKey, period.id)
: definition.formula
? (() => {
const left = valueForPeriod(rowByKey, definition.formula!.left, period.id);
const right = valueForPeriod(rowByKey, definition.formula!.right, period.id);
return left === null || right === null ? null : left - right;
})()
: sumValues(contributingRows.map((row) => row.values[period.id] ?? null));
return [period.id, nextValue];
})) satisfies Record<string, number | null>;
if (!rowHasAnyValue({ values })) {
continue;
}
const sourceConcepts = [...new Set(contributingRows.flatMap((row) => row.sourceConcepts))].sort((left, right) => left.localeCompare(right));
const sourceRowKeys = [...new Set(contributingRows.flatMap((row) => row.sourceRowKeys))].sort((left, right) => left.localeCompare(right));
const sourceFactIds = [...new Set(contributingRows.flatMap((row) => row.sourceFactIds))].sort((left, right) => left - right);
const hasDimensions = contributingRows.some((row) => row.hasDimensions);
const resolvedSourceRowKeys = Object.fromEntries(input.periods.map((period) => [
period.id,
definition.rowKey
? rowByKey.get(definition.rowKey)?.resolvedSourceRowKeys[period.id] ?? null
: null
]));
const rowsForDetail = definition.componentKeys
? contributingRows
: [];
const details = rowsForDetail
.filter((row) => rowHasAnyValue(row))
.map((row) => buildDetailRow({
row,
parentSurfaceKey: definition.key,
faithfulRowByKey
}));
statementDetails[definition.key] = details;
detailRowCount += details.length;
surfaceRows[statement].push({
key: definition.key,
label: definition.label,
category: definition.category,
templateSection: definition.category,
order: definition.order,
unit: definition.unit,
values,
sourceConcepts,
sourceRowKeys,
sourceFactIds,
formulaKey: definition.formula ? definition.key : null,
hasDimensions,
resolvedSourceRowKeys,
statement,
detailCount: details.length
});
surfaceRowCount += 1;
}
const baseline = baselineForStatement(statement, rowByKey);
const threshold = materialityThreshold(statement, baseline);
const residualRows = fullRows
.filter((row) => row.key.startsWith('other:'))
.filter((row) => rowHasAnyValue(row))
.map((row) => buildDetailRow({
row,
parentSurfaceKey: 'unmapped',
faithfulRowByKey
}));
if (residualRows.length > 0) {
statementDetails.unmapped = residualRows;
detailRowCount += residualRows.length;
unmappedRowCount += residualRows.length;
materialUnmappedRowCount += residualRows.filter((row) => maxAbsValue(row.values) >= threshold).length;
}
detailRows[statement] = statementDetails;
}
const normalizationSummary: NormalizationSummary = {
surfaceRowCount,
detailRowCount,
kpiRowCount: input.kpiRows?.length ?? 0,
unmappedRowCount,
materialUnmappedRowCount,
warnings: []
};
return {
surfaceRows,
detailRows,
normalizationSummary
};
}