import { sql } from 'drizzle-orm'; import { index, integer, numeric, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'; import type { TaskStageContext } from '@/lib/types'; type FilingMetrics = { revenue: number | null; netIncome: number | null; totalAssets: number | null; cash: number | null; debt: number | null; }; type TaxonomyAssetType = | 'instance' | 'schema' | 'presentation' | 'label' | 'calculation' | 'definition' | 'pdf' | 'other'; type TaxonomyParseStatus = 'ready' | 'partial' | 'failed'; type TaxonomyMetricValidationStatus = 'not_run' | 'matched' | 'mismatch' | 'error'; type CoverageStatus = 'backlog' | 'active' | 'watch' | 'archive'; type CoveragePriority = 'low' | 'medium' | 'high'; type ResearchJournalEntryType = 'note' | 'filing_note' | 'status_change'; type ResearchArtifactKind = 'filing' | 'ai_report' | 'note' | 'upload' | 'memo_snapshot' | 'status_change'; type ResearchArtifactSource = 'system' | 'user'; type ResearchVisibilityScope = 'private' | 'organization'; type ResearchMemoRating = 'strong_buy' | 'buy' | 'hold' | 'sell'; type ResearchMemoConviction = 'low' | 'medium' | 'high'; type ResearchMemoSection = | 'thesis' | 'variant_view' | 'catalysts' | 'risks' | 'disconfirming_evidence' | 'next_actions'; type FinancialCadence = 'annual' | 'quarterly' | 'ltm'; type SearchDocumentScope = 'global' | 'user'; type SearchDocumentSourceKind = 'filing_document' | 'filing_brief' | 'research_note'; type SearchIndexStatus = 'pending' | 'indexed' | 'failed'; type FinancialSurfaceKind = | 'income_statement' | 'balance_sheet' | 'cash_flow_statement' | 'ratios' | 'segments_kpis' | 'adjusted' | 'custom_metrics'; type FilingAnalysis = { provider?: string; model?: string; text?: string; legacyInsights?: string; extraction?: { summary: string; keyPoints: string[]; redFlags: string[]; followUpQuestions: string[]; portfolioSignals: string[]; segmentSpecificData: string[]; geographicRevenueBreakdown: string[]; companySpecificData: string[]; secApiCrossChecks: string[]; confidence: number; }; extractionMeta?: { provider: string; model: string; source: 'primary_document' | 'metadata_fallback'; generatedAt: string; }; }; type FinancialStatementKind = 'income' | 'balance' | 'cash_flow' | 'equity' | 'comprehensive_income'; type FilingStatementPeriod = { id: string; filingId: number; accessionNumber: string; filingDate: string; periodStart: string | null; periodEnd: string | null; filingType: '10-K' | '10-Q'; periodLabel: string; }; type StatementValuesByPeriod = Record; type FilingFaithfulStatementSnapshotRow = { key: string; label: string; concept: string | null; order: number; depth: number; isSubtotal: boolean; values: StatementValuesByPeriod; }; type StandardizedStatementSnapshotRow = { key: string; label: string; concept: string; category: string; sourceConcepts: string[]; values: StatementValuesByPeriod; }; type DimensionStatementSnapshotRow = { rowKey: string; concept: string | null; periodId: string; axis: string; member: string; value: number | null; unit: string | null; }; type FilingStatementBundle = { periods: FilingStatementPeriod[]; statements: Record; }; type StandardizedStatementBundle = { periods: FilingStatementPeriod[]; statements: Record; }; type DimensionStatementBundle = { statements: Record; }; type TaxonomyDimensionMember = { axis: string; member: string; }; type TaxonomyStatementSnapshotRow = { key: string; label: string; conceptKey: string; qname: string; namespaceUri: string; localName: string; isExtension: boolean; statement: FinancialStatementKind; roleUri: string | null; order: number; depth: number; parentKey: string | null; values: Record; units: Record; hasDimensions: boolean; sourceFactIds: number[]; }; type TaxonomyStatementBundle = { periods: FilingStatementPeriod[]; statements: Record; }; type TaxonomySurfaceSnapshotRow = { key: string; label: string; category: string; templateSection?: string; order: number; unit: 'currency' | 'count' | 'shares' | 'percent' | 'ratio'; values: Record; sourceConcepts: string[]; sourceRowKeys: string[]; sourceFactIds: number[]; formulaKey: string | null; hasDimensions: boolean; resolvedSourceRowKeys: Record; statement?: 'income' | 'balance' | 'cash_flow'; detailCount?: number; }; type TaxonomyDetailSnapshotRow = { key: string; parentSurfaceKey: string; label: string; conceptKey: string; qname: string; namespaceUri: string; localName: string; unit: string | null; values: Record; sourceFactIds: number[]; isExtension: boolean; dimensionsSummary: string[]; residualFlag: boolean; }; type TaxonomyDetailStatementMap = Record; type StructuredKpiSnapshotRow = { key: string; label: string; category: string; unit: 'currency' | 'count' | 'shares' | 'percent' | 'ratio'; order: number; segment: string | null; axis: string | null; member: string | null; values: Record; sourceConcepts: string[]; sourceFactIds: number[]; provenanceType: 'taxonomy' | 'structured_note'; hasDimensions: boolean; }; type TaxonomyNormalizationSummary = { surfaceRowCount: number; detailRowCount: number; kpiRowCount: number; unmappedRowCount: number; materialUnmappedRowCount: number; warnings: string[]; }; type TaxonomyMetricValidationCheck = { metricKey: keyof FilingMetrics; taxonomyValue: number | null; llmValue: number | null; absoluteDiff: number | null; relativeDiff: number | null; status: TaxonomyMetricValidationStatus; evidencePages: number[]; pdfUrl: string | null; provider: string | null; model: string | null; error: string | null; }; type TaxonomyMetricValidationResult = { status: TaxonomyMetricValidationStatus; checks: TaxonomyMetricValidationCheck[]; validatedAt: string | null; }; const authDateColumn = { mode: 'timestamp_ms' } as const; export const user = sqliteTable('user', { id: text('id').primaryKey().notNull(), name: text('name').notNull(), email: text('email').notNull(), emailVerified: integer('emailVerified', { mode: 'boolean' }).notNull().default(false), image: text('image'), createdAt: integer('createdAt', authDateColumn).notNull(), updatedAt: integer('updatedAt', authDateColumn).notNull(), role: text('role'), banned: integer('banned', { mode: 'boolean' }).default(false), banReason: text('banReason'), banExpires: integer('banExpires', authDateColumn) }, (table) => ({ userEmailUnique: uniqueIndex('user_email_uidx').on(table.email) })); export const organization = sqliteTable('organization', { id: text('id').primaryKey().notNull(), name: text('name').notNull(), slug: text('slug').notNull(), logo: text('logo'), createdAt: integer('createdAt', authDateColumn).notNull(), metadata: text('metadata') }, (table) => ({ organizationSlugUnique: uniqueIndex('organization_slug_uidx').on(table.slug) })); export const session = sqliteTable('session', { id: text('id').primaryKey().notNull(), expiresAt: integer('expiresAt', authDateColumn).notNull(), token: text('token').notNull(), createdAt: integer('createdAt', authDateColumn).notNull(), updatedAt: integer('updatedAt', authDateColumn).notNull(), ipAddress: text('ipAddress'), userAgent: text('userAgent'), userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }), impersonatedBy: text('impersonatedBy'), activeOrganizationId: text('activeOrganizationId') }, (table) => ({ sessionTokenUnique: uniqueIndex('session_token_uidx').on(table.token), sessionUserIdIndex: index('session_userId_idx').on(table.userId) })); export const account = sqliteTable('account', { id: text('id').primaryKey().notNull(), accountId: text('accountId').notNull(), providerId: text('providerId').notNull(), userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }), accessToken: text('accessToken'), refreshToken: text('refreshToken'), idToken: text('idToken'), accessTokenExpiresAt: integer('accessTokenExpiresAt', authDateColumn), refreshTokenExpiresAt: integer('refreshTokenExpiresAt', authDateColumn), scope: text('scope'), password: text('password'), createdAt: integer('createdAt', authDateColumn).notNull(), updatedAt: integer('updatedAt', authDateColumn).notNull() }, (table) => ({ accountUserIdIndex: index('account_userId_idx').on(table.userId) })); export const verification = sqliteTable('verification', { id: text('id').primaryKey().notNull(), identifier: text('identifier').notNull(), value: text('value').notNull(), expiresAt: integer('expiresAt', authDateColumn).notNull(), createdAt: integer('createdAt', authDateColumn).notNull(), updatedAt: integer('updatedAt', authDateColumn).notNull() }, (table) => ({ verificationIdentifierIndex: index('verification_identifier_idx').on(table.identifier) })); export const member = sqliteTable('member', { id: text('id').primaryKey().notNull(), organizationId: text('organizationId').notNull().references(() => organization.id, { onDelete: 'cascade' }), userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }), role: text('role').notNull().default('member'), createdAt: integer('createdAt', authDateColumn).notNull() }, (table) => ({ memberOrganizationIdIndex: index('member_organizationId_idx').on(table.organizationId), memberUserIdIndex: index('member_userId_idx').on(table.userId) })); export const invitation = sqliteTable('invitation', { id: text('id').primaryKey().notNull(), organizationId: text('organizationId').notNull().references(() => organization.id, { onDelete: 'cascade' }), email: text('email').notNull(), role: text('role'), status: text('status').notNull().default('pending'), expiresAt: integer('expiresAt', authDateColumn).notNull(), createdAt: integer('createdAt', authDateColumn).notNull(), inviterId: text('inviterId').notNull().references(() => user.id, { onDelete: 'cascade' }) }, (table) => ({ invitationOrganizationIdIndex: index('invitation_organizationId_idx').on(table.organizationId), invitationEmailIndex: index('invitation_email_idx').on(table.email) })); export const watchlistItem = sqliteTable('watchlist_item', { id: integer('id').primaryKey({ autoIncrement: true }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), ticker: text('ticker').notNull(), company_name: text('company_name').notNull(), sector: text('sector'), category: text('category'), tags: text('tags', { mode: 'json' }).$type(), status: text('status').$type().notNull().default('backlog'), priority: text('priority').$type().notNull().default('medium'), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull(), last_reviewed_at: text('last_reviewed_at') }, (table) => ({ watchlistUserTickerUnique: uniqueIndex('watchlist_user_ticker_uidx').on(table.user_id, table.ticker), watchlistUserCreatedIndex: index('watchlist_user_created_idx').on(table.user_id, table.created_at), watchlistUserUpdatedIndex: index('watchlist_user_updated_idx').on(table.user_id, table.updated_at) })); export const holding = sqliteTable('holding', { id: integer('id').primaryKey({ autoIncrement: true }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), ticker: text('ticker').notNull(), company_name: text('company_name'), shares: numeric('shares').notNull(), avg_cost: numeric('avg_cost').notNull(), current_price: numeric('current_price'), market_value: numeric('market_value').notNull(), gain_loss: numeric('gain_loss').notNull(), gain_loss_pct: numeric('gain_loss_pct').notNull(), last_price_at: text('last_price_at'), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ holdingUserTickerUnique: uniqueIndex('holding_user_ticker_uidx').on(table.user_id, table.ticker), holdingUserIndex: index('holding_user_idx').on(table.user_id) })); export const filing = sqliteTable('filing', { id: integer('id').primaryKey({ autoIncrement: true }), ticker: text('ticker').notNull(), filing_type: text('filing_type').$type<'10-K' | '10-Q' | '8-K'>().notNull(), filing_date: text('filing_date').notNull(), accession_number: text('accession_number').notNull(), cik: text('cik').notNull(), company_name: text('company_name').notNull(), filing_url: text('filing_url'), submission_url: text('submission_url'), primary_document: text('primary_document'), metrics: text('metrics', { mode: 'json' }).$type(), analysis: text('analysis', { mode: 'json' }).$type(), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ filingAccessionUnique: uniqueIndex('filing_accession_uidx').on(table.accession_number), filingTickerDateIndex: index('filing_ticker_date_idx').on(table.ticker, table.filing_date), filingDateIndex: index('filing_date_idx').on(table.filing_date) })); export const filingStatementSnapshot = sqliteTable('filing_statement_snapshot', { id: integer('id').primaryKey({ autoIncrement: true }), filing_id: integer('filing_id').notNull().references(() => filing.id, { onDelete: 'cascade' }), ticker: text('ticker').notNull(), filing_date: text('filing_date').notNull(), filing_type: text('filing_type').$type<'10-K' | '10-Q'>().notNull(), period_end: text('period_end'), statement_bundle: text('statement_bundle', { mode: 'json' }).$type(), standardized_bundle: text('standardized_bundle', { mode: 'json' }).$type(), dimension_bundle: text('dimension_bundle', { mode: 'json' }).$type(), parse_status: text('parse_status').$type<'ready' | 'partial' | 'failed'>().notNull(), parse_error: text('parse_error'), source: text('source').$type<'sec_filing_summary' | 'xbrl_instance' | 'companyfacts_fallback'>().notNull(), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ filingStatementFilingUnique: uniqueIndex('filing_stmt_filing_uidx').on(table.filing_id), filingStatementTickerDateIndex: index('filing_stmt_ticker_date_idx').on(table.ticker, table.filing_date), filingStatementDateIndex: index('filing_stmt_date_idx').on(table.filing_date), filingStatementStatusIndex: index('filing_stmt_status_idx').on(table.parse_status) })); export const filingTaxonomySnapshot = sqliteTable('filing_taxonomy_snapshot', { id: integer('id').primaryKey({ autoIncrement: true }), filing_id: integer('filing_id').notNull().references(() => filing.id, { onDelete: 'cascade' }), ticker: text('ticker').notNull(), filing_date: text('filing_date').notNull(), filing_type: text('filing_type').$type<'10-K' | '10-Q'>().notNull(), parse_status: text('parse_status').$type().notNull(), parse_error: text('parse_error'), source: text('source').$type<'xbrl_instance' | 'xbrl_instance_with_linkbase' | 'legacy_html_fallback'>().notNull(), parser_engine: text('parser_engine').notNull().default('fiscal-xbrl'), parser_version: text('parser_version').notNull().default('unknown'), taxonomy_regime: text('taxonomy_regime').$type<'us-gaap' | 'ifrs-full' | 'unknown'>().notNull().default('unknown'), fiscal_pack: text('fiscal_pack'), periods: text('periods', { mode: 'json' }).$type(), faithful_rows: text('faithful_rows', { mode: 'json' }).$type(), statement_rows: text('statement_rows', { mode: 'json' }).$type(), surface_rows: text('surface_rows', { mode: 'json' }).$type | null>(), detail_rows: text('detail_rows', { mode: 'json' }).$type | null>(), kpi_rows: text('kpi_rows', { mode: 'json' }).$type(), derived_metrics: text('derived_metrics', { mode: 'json' }).$type(), validation_result: text('validation_result', { mode: 'json' }).$type(), normalization_summary: text('normalization_summary', { mode: 'json' }).$type(), facts_count: integer('facts_count').notNull().default(0), concepts_count: integer('concepts_count').notNull().default(0), dimensions_count: integer('dimensions_count').notNull().default(0), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ filingTaxonomySnapshotFilingUnique: uniqueIndex('filing_taxonomy_snapshot_filing_uidx').on(table.filing_id), filingTaxonomySnapshotTickerDateIndex: index('filing_taxonomy_snapshot_ticker_date_idx').on(table.ticker, table.filing_date), filingTaxonomySnapshotStatusIndex: index('filing_taxonomy_snapshot_status_idx').on(table.parse_status) })); export const filingTaxonomyContext = sqliteTable('filing_taxonomy_context', { id: integer('id').primaryKey({ autoIncrement: true }), snapshot_id: integer('snapshot_id').notNull().references(() => filingTaxonomySnapshot.id, { onDelete: 'cascade' }), context_id: text('context_id').notNull(), entity_identifier: text('entity_identifier'), entity_scheme: text('entity_scheme'), period_start: text('period_start'), period_end: text('period_end'), period_instant: text('period_instant'), segment_json: text('segment_json', { mode: 'json' }).$type | null>(), scenario_json: text('scenario_json', { mode: 'json' }).$type | null>(), created_at: text('created_at').notNull() }, (table) => ({ filingTaxonomyContextSnapshotIndex: index('filing_taxonomy_context_snapshot_idx').on(table.snapshot_id), filingTaxonomyContextUnique: uniqueIndex('filing_taxonomy_context_uidx').on(table.snapshot_id, table.context_id) })); export const filingTaxonomyAsset = sqliteTable('filing_taxonomy_asset', { id: integer('id').primaryKey({ autoIncrement: true }), snapshot_id: integer('snapshot_id').notNull().references(() => filingTaxonomySnapshot.id, { onDelete: 'cascade' }), asset_type: text('asset_type').$type().notNull(), name: text('name').notNull(), url: text('url').notNull(), size_bytes: integer('size_bytes'), score: numeric('score'), is_selected: integer('is_selected', { mode: 'boolean' }).notNull().default(false), created_at: text('created_at').notNull() }, (table) => ({ filingTaxonomyAssetSnapshotIndex: index('filing_taxonomy_asset_snapshot_idx').on(table.snapshot_id), filingTaxonomyAssetTypeIndex: index('filing_taxonomy_asset_type_idx').on(table.snapshot_id, table.asset_type) })); export const filingTaxonomyConcept = sqliteTable('filing_taxonomy_concept', { id: integer('id').primaryKey({ autoIncrement: true }), snapshot_id: integer('snapshot_id').notNull().references(() => filingTaxonomySnapshot.id, { onDelete: 'cascade' }), concept_key: text('concept_key').notNull(), qname: text('qname').notNull(), namespace_uri: text('namespace_uri').notNull(), local_name: text('local_name').notNull(), label: text('label'), is_extension: integer('is_extension', { mode: 'boolean' }).notNull().default(false), balance: text('balance'), period_type: text('period_type'), data_type: text('data_type'), statement_kind: text('statement_kind').$type(), role_uri: text('role_uri'), authoritative_concept_key: text('authoritative_concept_key'), mapping_method: text('mapping_method'), surface_key: text('surface_key'), detail_parent_surface_key: text('detail_parent_surface_key'), kpi_key: text('kpi_key'), residual_flag: integer('residual_flag', { mode: 'boolean' }).notNull().default(false), presentation_order: numeric('presentation_order'), presentation_depth: integer('presentation_depth'), parent_concept_key: text('parent_concept_key'), is_abstract: integer('is_abstract', { mode: 'boolean' }).notNull().default(false), created_at: text('created_at').notNull() }, (table) => ({ filingTaxonomyConceptSnapshotIndex: index('filing_taxonomy_concept_snapshot_idx').on(table.snapshot_id), filingTaxonomyConceptStatementIndex: index('filing_taxonomy_concept_statement_idx').on(table.snapshot_id, table.statement_kind), filingTaxonomyConceptUnique: uniqueIndex('filing_taxonomy_concept_uidx').on( table.snapshot_id, table.concept_key, table.role_uri, table.presentation_order ) })); export const filingTaxonomyFact = sqliteTable('filing_taxonomy_fact', { id: integer('id').primaryKey({ autoIncrement: true }), snapshot_id: integer('snapshot_id').notNull().references(() => filingTaxonomySnapshot.id, { onDelete: 'cascade' }), concept_key: text('concept_key').notNull(), qname: text('qname').notNull(), namespace_uri: text('namespace_uri').notNull(), local_name: text('local_name').notNull(), data_type: text('data_type'), statement_kind: text('statement_kind').$type(), role_uri: text('role_uri'), authoritative_concept_key: text('authoritative_concept_key'), mapping_method: text('mapping_method'), surface_key: text('surface_key'), detail_parent_surface_key: text('detail_parent_surface_key'), kpi_key: text('kpi_key'), residual_flag: integer('residual_flag', { mode: 'boolean' }).notNull().default(false), context_id: text('context_id').notNull(), unit: text('unit'), decimals: text('decimals'), precision: text('precision'), nil: integer('nil', { mode: 'boolean' }).notNull().default(false), value_num: numeric('value_num').notNull(), period_start: text('period_start'), period_end: text('period_end'), period_instant: text('period_instant'), dimensions: text('dimensions', { mode: 'json' }).$type().notNull(), is_dimensionless: integer('is_dimensionless', { mode: 'boolean' }).notNull().default(true), source_file: text('source_file'), created_at: text('created_at').notNull() }, (table) => ({ filingTaxonomyFactSnapshotIndex: index('filing_taxonomy_fact_snapshot_idx').on(table.snapshot_id), filingTaxonomyFactConceptIndex: index('filing_taxonomy_fact_concept_idx').on(table.snapshot_id, table.concept_key), filingTaxonomyFactPeriodIndex: index('filing_taxonomy_fact_period_idx').on(table.snapshot_id, table.period_end, table.period_instant), filingTaxonomyFactStatementIndex: index('filing_taxonomy_fact_statement_idx').on(table.snapshot_id, table.statement_kind) })); export const filingTaxonomyMetricValidation = sqliteTable('filing_taxonomy_metric_validation', { id: integer('id').primaryKey({ autoIncrement: true }), snapshot_id: integer('snapshot_id').notNull().references(() => filingTaxonomySnapshot.id, { onDelete: 'cascade' }), metric_key: text('metric_key').$type().notNull(), taxonomy_value: numeric('taxonomy_value'), llm_value: numeric('llm_value'), absolute_diff: numeric('absolute_diff'), relative_diff: numeric('relative_diff'), status: text('status').$type().notNull(), evidence_pages: text('evidence_pages', { mode: 'json' }).$type().notNull(), pdf_url: text('pdf_url'), provider: text('provider'), model: text('model'), error: text('error'), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ filingTaxonomyMetricValidationSnapshotIndex: index('filing_taxonomy_metric_validation_snapshot_idx').on(table.snapshot_id), filingTaxonomyMetricValidationStatusIndex: index('filing_taxonomy_metric_validation_status_idx').on(table.snapshot_id, table.status), filingTaxonomyMetricValidationUnique: uniqueIndex('filing_taxonomy_metric_validation_uidx').on(table.snapshot_id, table.metric_key) })); export const companyFinancialBundle = sqliteTable('company_financial_bundle', { id: integer('id').primaryKey({ autoIncrement: true }), ticker: text('ticker').notNull(), surface_kind: text('surface_kind').$type().notNull(), cadence: text('cadence').$type().notNull(), bundle_version: integer('bundle_version').notNull(), source_snapshot_ids: text('source_snapshot_ids', { mode: 'json' }).$type().notNull(), source_signature: text('source_signature').notNull(), payload: text('payload', { mode: 'json' }).$type>().notNull(), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ companyFinancialBundleUnique: uniqueIndex('company_financial_bundle_uidx').on(table.ticker, table.surface_kind, table.cadence), companyFinancialBundleTickerIndex: index('company_financial_bundle_ticker_idx').on(table.ticker, table.updated_at) })); export const companyOverviewCache = sqliteTable('company_overview_cache', { id: integer('id').primaryKey({ autoIncrement: true }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), ticker: text('ticker').notNull(), cache_version: integer('cache_version').notNull(), source_signature: text('source_signature').notNull(), payload: text('payload', { mode: 'json' }).$type>().notNull(), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ companyOverviewCacheUnique: uniqueIndex('company_overview_cache_uidx').on(table.user_id, table.ticker), companyOverviewCacheLookupIndex: index('company_overview_cache_lookup_idx').on(table.user_id, table.ticker, table.updated_at) })); export const filingLink = sqliteTable('filing_link', { id: integer('id').primaryKey({ autoIncrement: true }), filing_id: integer('filing_id').notNull().references(() => filing.id, { onDelete: 'cascade' }), link_type: text('link_type').notNull(), url: text('url').notNull(), source: text('source').notNull().default('sec'), created_at: text('created_at').notNull() }, (table) => ({ filingLinkUnique: uniqueIndex('filing_link_unique_uidx').on(table.filing_id, table.url), filingLinkFilingIndex: index('filing_link_filing_idx').on(table.filing_id) })); export const taskRun = sqliteTable('task_run', { id: text('id').primaryKey().notNull(), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), task_type: text('task_type').$type<'sync_filings' | 'refresh_prices' | 'analyze_filing' | 'portfolio_insights' | 'index_search'>().notNull(), status: text('status').$type<'queued' | 'running' | 'completed' | 'failed'>().notNull(), stage: text('stage').notNull(), stage_detail: text('stage_detail'), stage_context: text('stage_context', { mode: 'json' }).$type(), resource_key: text('resource_key'), notification_read_at: text('notification_read_at'), notification_silenced_at: text('notification_silenced_at'), priority: integer('priority').notNull(), payload: text('payload', { mode: 'json' }).$type>().notNull(), result: text('result', { mode: 'json' }).$type | null>(), error: text('error'), attempts: integer('attempts').notNull(), max_attempts: integer('max_attempts').notNull(), workflow_run_id: text('workflow_run_id'), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull(), finished_at: text('finished_at') }, (table) => ({ taskUserCreatedIndex: index('task_user_created_idx').on(table.user_id, table.created_at), taskUserUpdatedIndex: index('task_user_updated_idx').on(table.user_id, table.updated_at), taskStatusIndex: index('task_status_idx').on(table.status), taskUserResourceStatusIndex: index('task_user_resource_status_idx').on( table.user_id, table.task_type, table.resource_key, table.status, table.created_at ), taskWorkflowRunUnique: uniqueIndex('task_workflow_run_uidx').on(table.workflow_run_id) })); // Note: Partial unique index for active resource-scoped task deduplication is created via // migration 0013_task_active_resource_unique.sql. SQLite does not support partial indexes // in drizzle schema DSL, so the index is managed separately: // CREATE UNIQUE INDEX task_active_resource_uidx ON task_run (user_id, task_type, resource_key) // WHERE resource_key IS NOT NULL AND status IN ('queued', 'running'); export const taskStageEvent = sqliteTable('task_stage_event', { id: integer('id').primaryKey({ autoIncrement: true }), task_id: text('task_id').notNull().references(() => taskRun.id, { onDelete: 'cascade' }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), stage: text('stage').notNull(), stage_detail: text('stage_detail'), stage_context: text('stage_context', { mode: 'json' }).$type(), status: text('status').$type<'queued' | 'running' | 'completed' | 'failed'>().notNull(), created_at: text('created_at').notNull() }, (table) => ({ taskStageEventTaskCreatedIndex: index('task_stage_event_task_created_idx').on(table.task_id, table.created_at), taskStageEventUserCreatedIndex: index('task_stage_event_user_created_idx').on(table.user_id, table.created_at) })); export const portfolioInsight = sqliteTable('portfolio_insight', { id: integer('id').primaryKey({ autoIncrement: true }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), provider: text('provider').notNull(), model: text('model').notNull(), content: text('content').notNull(), created_at: text('created_at').notNull() }, (table) => ({ insightUserCreatedIndex: index('insight_user_created_idx').on(table.user_id, table.created_at) })); export const researchJournalEntry = sqliteTable('research_journal_entry', { id: integer('id').primaryKey({ autoIncrement: true }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), ticker: text('ticker').notNull(), accession_number: text('accession_number'), entry_type: text('entry_type').$type().notNull(), title: text('title'), body_markdown: text('body_markdown').notNull(), metadata: text('metadata', { mode: 'json' }).$type | null>(), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ researchJournalTickerIndex: index('research_journal_ticker_idx').on(table.user_id, table.ticker, table.created_at), researchJournalAccessionIndex: index('research_journal_accession_idx').on(table.user_id, table.accession_number) })); export const searchDocument = sqliteTable('search_document', { id: integer('id').primaryKey({ autoIncrement: true }), source_kind: text('source_kind').$type().notNull(), source_ref: text('source_ref').notNull(), scope: text('scope').$type().notNull(), user_id: text('user_id').references(() => user.id, { onDelete: 'cascade' }), ticker: text('ticker'), accession_number: text('accession_number'), title: text('title'), content_text: text('content_text').notNull(), content_hash: text('content_hash').notNull(), metadata: text('metadata', { mode: 'json' }).$type | null>(), index_status: text('index_status').$type().notNull(), indexed_at: text('indexed_at'), last_error: text('last_error'), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ searchDocumentSourceUnique: uniqueIndex('search_document_source_uidx').on( table.scope, sql`ifnull(${table.user_id}, '')`, table.source_kind, table.source_ref ), searchDocumentScopeIndex: index('search_document_scope_idx').on( table.scope, table.source_kind, table.ticker, table.updated_at ), searchDocumentAccessionIndex: index('search_document_accession_idx').on(table.accession_number, table.source_kind) })); export const searchChunk = sqliteTable('search_chunk', { id: integer('id').primaryKey({ autoIncrement: true }), document_id: integer('document_id').notNull().references(() => searchDocument.id, { onDelete: 'cascade' }), chunk_index: integer('chunk_index').notNull(), chunk_text: text('chunk_text').notNull(), char_count: integer('char_count').notNull(), start_offset: integer('start_offset').notNull(), end_offset: integer('end_offset').notNull(), heading_path: text('heading_path'), citation_label: text('citation_label').notNull(), created_at: text('created_at').notNull() }, (table) => ({ searchChunkUnique: uniqueIndex('search_chunk_document_chunk_uidx').on(table.document_id, table.chunk_index), searchChunkDocumentIndex: index('search_chunk_document_idx').on(table.document_id) })); export const researchArtifact = sqliteTable('research_artifact', { id: integer('id').primaryKey({ autoIncrement: true }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), organization_id: text('organization_id').references(() => organization.id, { onDelete: 'set null' }), ticker: text('ticker').notNull(), accession_number: text('accession_number'), kind: text('kind').$type().notNull(), source: text('source').$type().notNull().default('user'), subtype: text('subtype'), title: text('title'), summary: text('summary'), body_markdown: text('body_markdown'), search_text: text('search_text'), visibility_scope: text('visibility_scope').$type().notNull().default('private'), tags: text('tags', { mode: 'json' }).$type(), metadata: text('metadata', { mode: 'json' }).$type | null>(), file_name: text('file_name'), mime_type: text('mime_type'), file_size_bytes: integer('file_size_bytes'), storage_path: text('storage_path'), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ researchArtifactTickerIndex: index('research_artifact_ticker_idx').on(table.user_id, table.ticker, table.updated_at), researchArtifactKindIndex: index('research_artifact_kind_idx').on(table.user_id, table.kind, table.updated_at), researchArtifactAccessionIndex: index('research_artifact_accession_idx').on(table.user_id, table.accession_number), researchArtifactSourceIndex: index('research_artifact_source_idx').on(table.user_id, table.source, table.updated_at) })); export const researchMemo = sqliteTable('research_memo', { id: integer('id').primaryKey({ autoIncrement: true }), user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), organization_id: text('organization_id').references(() => organization.id, { onDelete: 'set null' }), ticker: text('ticker').notNull(), rating: text('rating').$type(), conviction: text('conviction').$type(), time_horizon_months: integer('time_horizon_months'), packet_title: text('packet_title'), packet_subtitle: text('packet_subtitle'), thesis_markdown: text('thesis_markdown').notNull().default(''), variant_view_markdown: text('variant_view_markdown').notNull().default(''), catalysts_markdown: text('catalysts_markdown').notNull().default(''), risks_markdown: text('risks_markdown').notNull().default(''), disconfirming_evidence_markdown: text('disconfirming_evidence_markdown').notNull().default(''), next_actions_markdown: text('next_actions_markdown').notNull().default(''), created_at: text('created_at').notNull(), updated_at: text('updated_at').notNull() }, (table) => ({ researchMemoTickerUnique: uniqueIndex('research_memo_ticker_uidx').on(table.user_id, table.ticker), researchMemoUpdatedIndex: index('research_memo_updated_idx').on(table.user_id, table.updated_at) })); export const researchMemoEvidence = sqliteTable('research_memo_evidence', { id: integer('id').primaryKey({ autoIncrement: true }), memo_id: integer('memo_id').notNull().references(() => researchMemo.id, { onDelete: 'cascade' }), artifact_id: integer('artifact_id').notNull().references(() => researchArtifact.id, { onDelete: 'cascade' }), section: text('section').$type().notNull(), annotation: text('annotation'), sort_order: integer('sort_order').notNull().default(0), created_at: text('created_at').notNull() }, (table) => ({ researchMemoEvidenceMemoIndex: index('research_memo_evidence_memo_idx').on(table.memo_id, table.section, table.sort_order), researchMemoEvidenceArtifactIndex: index('research_memo_evidence_artifact_idx').on(table.artifact_id), researchMemoEvidenceUnique: uniqueIndex('research_memo_evidence_unique_uidx').on(table.memo_id, table.artifact_id, table.section) })); export const authSchema = { user, session, account, verification, organization, member, invitation }; export const appSchema = { watchlistItem, holding, filing, filingStatementSnapshot, filingTaxonomySnapshot, filingTaxonomyAsset, filingTaxonomyConcept, filingTaxonomyFact, filingTaxonomyMetricValidation, companyFinancialBundle, companyOverviewCache, filingLink, taskRun, taskStageEvent, portfolioInsight, researchJournalEntry, searchDocument, searchChunk, researchArtifact, researchMemo, researchMemoEvidence }; export const schema = { ...authSchema, ...appSchema };