Merge branch 't3code/expand-research-management-plan'

# Conflicts:
#	app/analysis/page.tsx
#	app/watchlist/page.tsx
#	components/shell/app-shell.tsx
#	lib/api.ts
#	lib/query/options.ts
#	lib/server/api/app.ts
#	lib/server/db/index.test.ts
#	lib/server/db/index.ts
#	lib/server/db/schema.ts
#	lib/server/repos/research-journal.ts
#	lib/types.ts
This commit is contained in:
2026-03-07 20:39:49 -05:00
38 changed files with 5533 additions and 427 deletions

View File

@@ -31,6 +31,18 @@ type TaxonomyMetricValidationStatus = 'not_run' | 'matched' | 'mismatch' | 'erro
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';
@@ -623,6 +635,72 @@ export const searchChunk = sqliteTable('search_chunk', {
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<ResearchArtifactKind>().notNull(),
source: text('source').$type<ResearchArtifactSource>().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<ResearchVisibilityScope>().notNull().default('private'),
tags: text('tags', { mode: 'json' }).$type<string[]>(),
metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown> | 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<ResearchMemoRating>(),
conviction: text('conviction').$type<ResearchMemoConviction>(),
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<ResearchMemoSection>().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,
@@ -650,7 +728,10 @@ export const appSchema = {
portfolioInsight,
researchJournalEntry,
searchDocument,
searchChunk
searchChunk,
researchArtifact,
researchMemo,
researchMemoEvidence
};
export const schema = {