Add memo.addSection and memo.deleteSection RPC methods with full-stack implementation: contract types, Zod schemas, DB queries, RPC handlers, UI buttons, and tests. - memo.addSection: creates a new empty section, supports positional insert after a given section via afterSectionId - memo.deleteSection: removes a section and cascades cleanup of its annotations, citations, and section reviews - Outline sidebar: per-section delete button, + Add Section button - Article area: + Add Section button after last section - 9 new tests (15 total memo tests, 48 project-wide)
457 lines
14 KiB
TypeScript
457 lines
14 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const ScreenSchema = z.enum(["home", "workspace", "model", "memo", "agents"]);
|
|
export type Screen = z.infer<typeof ScreenSchema>;
|
|
|
|
export const HoldingSchema = z.object({
|
|
ticker: z.string(),
|
|
name: z.string(),
|
|
price: z.number(),
|
|
changePct: z.number(),
|
|
weight: z.number()
|
|
});
|
|
export type Holding = z.infer<typeof HoldingSchema>;
|
|
|
|
export const AgentSchema = z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
status: z.enum(["idle", "queued", "running", "completed", "paused", "failed"]),
|
|
progress: z.number().min(0).max(100),
|
|
action: z.string(),
|
|
pipeline: z.enum(["research", "competitive", "cross-cutting"]).optional(),
|
|
confidence: z.enum(["high", "medium", "low"]).optional()
|
|
});
|
|
export type Agent = z.infer<typeof AgentSchema>;
|
|
|
|
export const CompanySchema = z.object({
|
|
id: z.string(),
|
|
ticker: z.string(),
|
|
name: z.string(),
|
|
sector: z.string(),
|
|
subIndustry: z.string().optional(),
|
|
price: z.number(),
|
|
changePct: z.number(),
|
|
thesis: z.string(),
|
|
founded: z.string().optional(),
|
|
headquarters: z.string().optional(),
|
|
employees: z.number().optional()
|
|
});
|
|
export type Company = z.infer<typeof CompanySchema>;
|
|
|
|
export const ModelRowSchema = z.object({
|
|
label: z.string(),
|
|
kind: z.enum(["actual", "forecast", "total"]),
|
|
values: z.array(z.string())
|
|
});
|
|
export type ModelRow = z.infer<typeof ModelRowSchema>;
|
|
|
|
export const MemoSectionSchema = z.object({
|
|
id: z.string(),
|
|
title: z.string(),
|
|
content: z.string(),
|
|
updatedAt: z.string().optional(),
|
|
primaryAgent: z.string().optional()
|
|
});
|
|
export type MemoSection = z.infer<typeof MemoSectionSchema>;
|
|
|
|
export const MemoCitationSchema = z.object({
|
|
id: z.string(),
|
|
label: z.string(),
|
|
sectionId: z.string(),
|
|
type: z.enum(["sec_filing", "earnings_transcript", "analyst_report", "model", "internal_note"]),
|
|
title: z.string(),
|
|
reference: z.string(),
|
|
verificationStatus: z.enum(["verified", "unverified", "flagged"]),
|
|
sourceUrl: z.string().optional()
|
|
});
|
|
export type MemoCitation = z.infer<typeof MemoCitationSchema>;
|
|
|
|
export const MemoAnnotationSchema = z.object({
|
|
id: z.string(),
|
|
sectionId: z.string(),
|
|
kind: z.enum(["highlight", "comment", "strike"]),
|
|
selectedText: z.string(),
|
|
comment: z.string().optional(),
|
|
createdBy: z.string(),
|
|
createdAt: z.string(),
|
|
status: z.enum(["open", "resolved"])
|
|
});
|
|
export type MemoAnnotation = z.infer<typeof MemoAnnotationSchema>;
|
|
|
|
export const MemoSectionReviewSchema = z.object({
|
|
sectionId: z.string(),
|
|
status: z.enum(["pending", "in_review", "approved", "changes_requested"]),
|
|
updatedAt: z.string().optional()
|
|
});
|
|
export type MemoSectionReview = z.infer<typeof MemoSectionReviewSchema>;
|
|
|
|
export const MemoSchema = z.object({
|
|
status: z.enum(["draft", "review", "final"]),
|
|
sections: z.array(MemoSectionSchema),
|
|
citations: z.array(MemoCitationSchema),
|
|
annotations: z.array(MemoAnnotationSchema),
|
|
sectionReviews: z.array(MemoSectionReviewSchema)
|
|
});
|
|
export type Memo = z.infer<typeof MemoSchema>;
|
|
|
|
export const CatalystSchema = z.object({
|
|
id: z.string(),
|
|
date: z.string(),
|
|
event: z.string(),
|
|
impact: z.enum(["high", "medium", "low"]),
|
|
thesisRelevance: z.enum(["supports", "challenges", "neutral"]),
|
|
source: z.string().optional()
|
|
});
|
|
export type Catalyst = z.infer<typeof CatalystSchema>;
|
|
|
|
export const AlertSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string().optional(),
|
|
timestamp: z.string(),
|
|
type: z.enum(["filing", "price_move", "earnings_surprise", "peer_event"]),
|
|
description: z.string(),
|
|
thesisImpact: z.enum(["positive", "negative", "neutral"]),
|
|
status: z.enum(["new", "reviewed"]),
|
|
targetSection: z.string().optional()
|
|
});
|
|
export type Alert = z.infer<typeof AlertSchema>;
|
|
|
|
export const RiskSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string(),
|
|
risk: z.string(),
|
|
category: z.enum(["business", "financial", "competitive", "regulatory", "esg"]),
|
|
severity: z.enum(["high", "medium", "low"]),
|
|
likelihood: z.enum(["high", "medium", "low"]),
|
|
mitigation: z.string(),
|
|
status: z.enum(["open", "mitigated", "accepted"])
|
|
});
|
|
export type Risk = z.infer<typeof RiskSchema>;
|
|
|
|
export const EarningsScheduleSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string(),
|
|
quarter: z.string(),
|
|
expectedDate: z.string(),
|
|
timing: z.enum(["bmo", "amc"]).optional(),
|
|
actualRevenue: z.string().optional(),
|
|
expectedRevenue: z.string().optional(),
|
|
actualEps: z.string().optional(),
|
|
expectedEps: z.string().optional()
|
|
});
|
|
export type EarningsSchedule = z.infer<typeof EarningsScheduleSchema>;
|
|
|
|
export const FilingSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string(),
|
|
formType: z.string(),
|
|
filedDate: z.string(),
|
|
title: z.string(),
|
|
keyChanges: z.string().optional(),
|
|
reviewed: z.boolean().optional()
|
|
});
|
|
export type Filing = z.infer<typeof FilingSchema>;
|
|
|
|
export const ExportRecordSchema = z.object({
|
|
id: z.string(),
|
|
type: z.enum(["pdf", "excel", "ppt"]),
|
|
title: z.string(),
|
|
companyId: z.string().optional(),
|
|
format: z.string(),
|
|
fileSize: z.string().optional(),
|
|
status: z.enum(["processing", "complete", "failed"]),
|
|
createdAt: z.string(),
|
|
downloadUrl: z.string().optional()
|
|
});
|
|
export type ExportRecord = z.infer<typeof ExportRecordSchema>;
|
|
|
|
export const WorkspaceSectionSchema = z.object({
|
|
id: z.string(),
|
|
title: z.string(),
|
|
content: z.string(),
|
|
validationState: z.enum(["verified", "flagged", "unverified", "failed"]),
|
|
sourceAgent: z.string().optional()
|
|
});
|
|
export type WorkspaceSection = z.infer<typeof WorkspaceSectionSchema>;
|
|
|
|
export const SnapshotSchema = z.object({
|
|
id: z.string(),
|
|
timestamp: z.string(),
|
|
label: z.string().optional(),
|
|
type: z.enum(["auto", "manual"]),
|
|
changeCount: z.number()
|
|
});
|
|
export type Snapshot = z.infer<typeof SnapshotSchema>;
|
|
|
|
export type ClientSettings = z.infer<typeof import("./rpcSchemas.js").ClientSettingsSchema>;
|
|
|
|
export type ServerSettings = z.infer<typeof import("./rpcSchemas.js").ServerSettingsSchema>;
|
|
|
|
export type RpcRequestMap = {
|
|
"portfolio.get": undefined;
|
|
"portfolio.addHolding": { ticker: string };
|
|
"portfolio.removeHolding": { ticker: string };
|
|
"company.get": { companyId: string };
|
|
"company.search": { query: string };
|
|
"company.setActive": { companyId: string };
|
|
"workspace.getSection": { companyId: string; section: string };
|
|
"workspace.listSources": { companyId: string };
|
|
"workspace.updateSection": { companyId: string; section: string; content: string };
|
|
"catalyst.list": { companyId: string };
|
|
"alert.list": { companyId?: string; since?: string };
|
|
"risk.list": { companyId: string };
|
|
"risk.add": { companyId: string; risk: Omit<Risk, "id" | "companyId"> };
|
|
"earnings.getSchedule": { companyId: string };
|
|
"filing.list": { companyId: string; since?: string };
|
|
"model.get": { companyId: string; tab: string };
|
|
"model.list": undefined;
|
|
"model.set": { provider: string; modelId: string; thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" };
|
|
"model.updateCell": { companyId: string; tab: string; row: number; col: number; value: string };
|
|
"model.createRow": { companyId: string; tab: string; label: string; kind: "actual" | "forecast" | "total"; values?: string[] };
|
|
"model.deleteRow": { companyId: string; tab: string; row: number };
|
|
"model.runScenario": { companyId: string; scenario: string; overrides: Record<string, string> };
|
|
"memo.get": { companyId: string };
|
|
"memo.addSection": {
|
|
companyId: string;
|
|
title: string;
|
|
afterSectionId?: string;
|
|
};
|
|
"memo.deleteSection": {
|
|
companyId: string;
|
|
sectionId: string;
|
|
};
|
|
"memo.updateSection": {
|
|
companyId: string;
|
|
sectionId: string;
|
|
title?: string;
|
|
content: string;
|
|
};
|
|
"memo.addAnnotation": {
|
|
companyId: string;
|
|
sectionId: string;
|
|
kind: "highlight" | "comment" | "strike";
|
|
selectedText: string;
|
|
comment?: string;
|
|
};
|
|
"memo.resolveAnnotation": {
|
|
companyId: string;
|
|
annotationId: string;
|
|
};
|
|
"memo.updateSectionReview": {
|
|
companyId: string;
|
|
sectionId: string;
|
|
status: "pending" | "in_review" | "approved" | "changes_requested";
|
|
};
|
|
"memo.acceptEdit": { companyId: string; editId: string };
|
|
"memo.rejectEdit": { companyId: string; editId: string; reason?: string };
|
|
"agent.list": { companyId?: string };
|
|
"agent.start": { agentId: string; companyId: string };
|
|
"agent.pause": { agentId: string };
|
|
"agent.restart": { agentId: string };
|
|
"agent.chat": { agentId: string; message: string };
|
|
"agent.configure": { agentId: string; config: Record<string, unknown> };
|
|
"agent.getTrace": { agentId: string; runId: string };
|
|
"agent.runPipeline": { companyId: string; pipeline: string };
|
|
"validation.run": { companyId: string; agentType?: "sv" | "qa" | "rt" | "all" };
|
|
"validation.getStatus": { companyId: string; sectionId?: string };
|
|
"export.list": { companyId?: string };
|
|
"export.create": { type: "pdf" | "excel" | "ppt"; companyId: string; options?: Record<string, unknown> };
|
|
"export.download": { exportId: string };
|
|
"settings.get": { scope: "client" | "server" };
|
|
"settings.update": import("./rpcSchemas.js").SettingsUpdatePayload;
|
|
};
|
|
|
|
export type RpcResponseMap = {
|
|
"portfolio.get": { id: string; name: string; holdings: Holding[]; activeCompanyId: string };
|
|
"portfolio.addHolding": { holding: Holding };
|
|
"portfolio.removeHolding": { ok: boolean };
|
|
"company.get": { company: Company };
|
|
"company.search": { results: Array<{ ticker: string; name: string; sector: string }> };
|
|
"company.setActive": { ok: boolean };
|
|
"workspace.getSection": { content: WorkspaceSection; validationState: string };
|
|
"workspace.listSources": { sources: Array<{ type: string; title: string; metadata: string }> };
|
|
"workspace.updateSection": { content: WorkspaceSection; savedAt: string };
|
|
"catalyst.list": { catalysts: Catalyst[] };
|
|
"alert.list": { alerts: Alert[] };
|
|
"risk.list": { risks: Risk[] };
|
|
"risk.add": { risk: Risk };
|
|
"earnings.getSchedule": { schedule: EarningsSchedule[] };
|
|
"filing.list": { filings: Filing[] };
|
|
"model.get": { headers: string[]; rows: ModelRow[] };
|
|
"model.list": { models: Array<{ provider: string; modelId: string; name: string; available: boolean }> };
|
|
"model.set": { ok: boolean; provider: string; modelId: string };
|
|
"model.updateCell": { ok: boolean; affectedCells: string[] };
|
|
"model.createRow": { row: ModelRow; position: number };
|
|
"model.deleteRow": { ok: boolean };
|
|
"model.runScenario": { headers: string[]; rows: ModelRow[] };
|
|
"memo.get": {
|
|
status: "draft" | "review" | "final";
|
|
sections: MemoSection[];
|
|
citations: MemoCitation[];
|
|
annotations: MemoAnnotation[];
|
|
sectionReviews: MemoSectionReview[];
|
|
};
|
|
"memo.addSection": {
|
|
section: MemoSection;
|
|
};
|
|
"memo.deleteSection": {
|
|
ok: boolean;
|
|
};
|
|
"memo.updateSection": {
|
|
section: MemoSection;
|
|
status: "draft" | "review" | "final";
|
|
savedAt: string;
|
|
};
|
|
"memo.addAnnotation": { annotation: MemoAnnotation };
|
|
"memo.resolveAnnotation": { annotation: MemoAnnotation };
|
|
"memo.updateSectionReview": { review: MemoSectionReview };
|
|
"memo.acceptEdit": { ok: boolean };
|
|
"memo.rejectEdit": { ok: boolean };
|
|
"agent.list": { agents: Agent[] };
|
|
"agent.start": { runId: string };
|
|
"agent.pause": { ok: boolean };
|
|
"agent.restart": { runId: string };
|
|
"agent.chat": { response: string };
|
|
"agent.configure": { ok: boolean };
|
|
"agent.getTrace": { steps: Array<{ step: number; label: string; detail: string }> };
|
|
"agent.runPipeline": { runIds: string[] };
|
|
"validation.run": {
|
|
sourceVerification?: { passed: boolean; confidence: string; issues: Array<{ severity: string; message: string; suggestion?: string }>; notes: string; timestamp: string };
|
|
modelQA?: { passed: boolean; confidence: string; issues: Array<{ severity: string; message: string; suggestion?: string }>; notes: string; timestamp: string };
|
|
redTeam?: { passed: boolean; confidence: string; issues: Array<{ severity: string; message: string; suggestion?: string }>; notes: string; timestamp: string };
|
|
};
|
|
"validation.getStatus": { validationState: "verified" | "flagged" | "unverified" | "failed"; lastValidated?: string };
|
|
"export.list": { exports: ExportRecord[] };
|
|
"export.create": { exportId: string };
|
|
"export.download": { data: ArrayBuffer };
|
|
"settings.get": { settings: ClientSettings | ServerSettings };
|
|
"settings.update": { ok: boolean };
|
|
};
|
|
|
|
export type RpcMethod = keyof RpcRequestMap;
|
|
|
|
export type RpcError = {
|
|
code: "NOT_FOUND" | "VALIDATION_ERROR" | "INTERNAL_ERROR" | "AGENT_FAILED" | "CONFLICT" | "RATE_LIMITED";
|
|
message: string;
|
|
detail?: unknown;
|
|
};
|
|
|
|
export type RpcResult<T extends RpcMethod> =
|
|
| { ok: true; data: RpcResponseMap[T] }
|
|
| { ok: false; error: RpcError };
|
|
|
|
export type RpcClient = {
|
|
call<T extends RpcMethod>(method: T, payload: RpcRequestMap[T]): Promise<RpcResult<T>>;
|
|
};
|
|
|
|
// ============== SSE Events ==============
|
|
|
|
export type ServerEventType =
|
|
| "agent.progress"
|
|
| "agent.completed"
|
|
| "agent.failed"
|
|
| "agent.started"
|
|
| "agent.streaming"
|
|
| "validation.updated"
|
|
| "memo.updated"
|
|
| "model.updated";
|
|
|
|
export type AgentProgressEvent = {
|
|
type: "agent.progress";
|
|
data: {
|
|
runId: string;
|
|
agentId: string;
|
|
companyId: string;
|
|
progress: number;
|
|
action: string;
|
|
timestamp: string;
|
|
};
|
|
};
|
|
|
|
export type AgentCompletedEvent = {
|
|
type: "agent.completed";
|
|
data: {
|
|
runId: string;
|
|
agentId: string;
|
|
companyId: string;
|
|
output: unknown;
|
|
duration: number;
|
|
timestamp: string;
|
|
};
|
|
};
|
|
|
|
export type AgentFailedEvent = {
|
|
type: "agent.failed";
|
|
data: {
|
|
runId: string;
|
|
agentId: string;
|
|
companyId: string;
|
|
error: string;
|
|
timestamp: string;
|
|
};
|
|
};
|
|
|
|
export type AgentStartedEvent = {
|
|
type: "agent.started";
|
|
data: {
|
|
runId: string;
|
|
agentId: string;
|
|
companyId: string;
|
|
pipeline?: string;
|
|
timestamp: string;
|
|
};
|
|
};
|
|
|
|
export type AgentStreamingEvent = {
|
|
type: "agent.streaming";
|
|
data: {
|
|
runId: string;
|
|
agentId: string;
|
|
companyId: string;
|
|
chunk: string;
|
|
timestamp: string;
|
|
};
|
|
};
|
|
|
|
export type ValidationUpdatedEvent = {
|
|
type: "validation.updated";
|
|
data: {
|
|
companyId: string;
|
|
sectionId?: string;
|
|
validationState: "verified" | "flagged" | "unverified" | "failed";
|
|
agentId: string;
|
|
notes?: string;
|
|
timestamp: string;
|
|
};
|
|
};
|
|
|
|
export type MemoUpdatedEvent = {
|
|
type: "memo.updated";
|
|
data: {
|
|
companyId: string;
|
|
sectionId: string;
|
|
content: string;
|
|
updatedAt: string;
|
|
};
|
|
};
|
|
|
|
export type ModelUpdatedEvent = {
|
|
type: "model.updated";
|
|
data: {
|
|
companyId: string;
|
|
tab: string;
|
|
cell?: { row: number; col: number };
|
|
timestamp: string;
|
|
};
|
|
};
|
|
|
|
export type ServerEvent =
|
|
| AgentProgressEvent
|
|
| AgentCompletedEvent
|
|
| AgentFailedEvent
|
|
| AgentStartedEvent
|
|
| AgentStreamingEvent
|
|
| ValidationUpdatedEvent
|
|
| MemoUpdatedEvent
|
|
| ModelUpdatedEvent;
|