Files
MosaicIQ/packages/contracts/src/rpc.ts
francy51 0e5a31866f feat(memo): add and delete sections via RPC
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)
2026-05-15 00:27:09 -04:00

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;