export type ExhibitType = 'folder' | 'document' | 'note' | 'event' | 'party' export type PartyKind = 'person' | 'organization' export type OrganizationKind = 'business' | 'public_body' | 'association' | 'informal_group' | 'other' export type SourceFileType = 'image' | 'pdf' | 'web_capture' | 'email' | 'article' | 'filing' | 'price_list' | 'text' | 'file' export interface CanvasPlacement { x: number y: number width: number height: number rotation: number zIndex: number hidden: boolean } export interface ExhibitBase extends CanvasPlacement { id: string type: ExhibitType title: string } export interface DocumentRegion { id: string label: string excerpt: string date?: string } export interface FolderExhibit extends ExhibitBase { type: 'folder' content: string isOpen: boolean } export interface DocumentExhibit extends ExhibitBase { type: 'document' /** Author-mode reveal requirements. Omitted from play-mode payloads. */ requiredFlags?: string[] publishedAt?: string capturedAt?: string sourceUri?: string body: string[] regions: DocumentRegion[] assetId?: string fileName?: string mimeType?: string fileSize?: number fileType: SourceFileType metadata: Record } export interface DocumentUploadAnalysis { extractionStatus: 'succeeded' | 'unsupported' | 'failed' matchedFlags: string[] awardedFlags: string[] } export interface UploadedCaseDocument extends DocumentExhibit { /** Transient upload response data; it is not part of persisted exhibit state. */ analysis: DocumentUploadAnalysis } export interface NoteExhibit extends ExhibitBase { type: 'note' content: string } export interface EventExhibit extends ExhibitBase { type: 'event' content: string eventDate?: string } export interface PartyExhibit extends ExhibitBase { type: 'party' content: string partyKind: PartyKind organizationKind?: OrganizationKind aliases: string[] } export type Exhibit = FolderExhibit | DocumentExhibit | NoteExhibit | EventExhibit | PartyExhibit export type Evidence = Exclude export type CaseDocument = DocumentExhibit export type EvidenceType = Evidence['type'] interface ExhibitRelationBase { id: string fromExhibitId: string toExhibitId: string sortOrder: number note?: string } export interface FolderMembership extends ExhibitRelationBase { type: 'contains' } export interface EventSupportRelation extends ExhibitRelationBase { type: 'supports' } export interface PartyAssociationRelation extends ExhibitRelationBase { type: 'concerns' } export interface ProvenanceRelation extends ExhibitRelationBase { type: 'source'; sourceRegionId?: string } export type ExhibitRelation = FolderMembership | EventSupportRelation | PartyAssociationRelation | ProvenanceRelation export interface Connection { id: string fromExhibitId: string toExhibitId: string label?: string /** Percentage from slack (0) to taut (100). */ tightness?: number tagStyle?: 'luggage' | 'compact' /** Percentage along the thread from its source exhibit. */ tagPosition?: number /** Signed perpendicular distance from the thread, constrained by tightness. */ tagOffset?: number } export interface Viewport { x: number; y: number; zoom: number } export interface TimelineRange { start: string; end: string } export type BoardViewPlacement = | { mode: 'docked'; dockEdge: 'top' | 'right' | 'bottom' | 'left'; size: number } | { mode: 'canvas' | 'window'; x: number; y: number; width: number; height: number } export interface TimelineView { id: string type: 'timeline' placement: BoardViewPlacement visible: boolean zIndex: number rangeMode: 'auto' | 'fixed' range?: TimelineRange } export type BoardView = TimelineView export interface TemporalFact { id: string exhibitId: string kind: 'published' | 'captured' | 'occurred' | 'region_date' start: string end?: string label: string } export interface BriefConcept { id: string label: string context: string expectedPartyKind?: PartyKind resolvedPartyExhibitId?: string } export interface LevelBrief { body: string; concepts: BriefConcept[] } export interface CaseState { id: string title: string subtitle: string exhibits: Exhibit[] relations: ExhibitRelation[] connections: Connection[] views: BoardView[] viewport: Viewport brief: LevelBrief revision: number updatedAt?: string levelStatus?: string sourceTemplateVersionId?: string editingAllowed?: boolean /** Visible documents that have not previously played their arrival flourish. */ newlyVisibleDocumentIds?: string[] } export interface LevelFlag { key: string earnedAt?: string gatedDocumentCount: number } export interface EvidenceMatchAnchorDefinition { id: string phrase: string minimumSimilarity: number sortOrder: number } export interface EvidenceMatchRuleDefinition { id: string name: string flagKey: string matcherVersion: 'char_trigram_v1' minimumAnchorMatches: number enabled: boolean anchors: EvidenceMatchAnchorDefinition[] } export function isDocumentExhibit(exhibit: Exhibit): exhibit is DocumentExhibit { return exhibit.type === 'document' } export function isEvidenceExhibit(exhibit: Exhibit): exhibit is Evidence { return exhibit.type !== 'document' } export function isFolderExhibit(exhibit: Exhibit): exhibit is FolderExhibit { return exhibit.type === 'folder' } export function isEventExhibit(exhibit: Exhibit): exhibit is EventExhibit { return exhibit.type === 'event' } export function isPartyExhibit(exhibit: Exhibit): exhibit is PartyExhibit { return exhibit.type === 'party' }