export type ExhibitType = 'folder' | 'document' | 'note' | 'event' | 'party' | 'claim' 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 type DocumentCaptureKind = 'unclassified' | 'photo' | 'scene' | 'clipping' | 'full_page' 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' /** Stable, board-local number used when citing this exhibit. */ displayNumber?: number /** Author-mode reveal requirements. Omitted from play-mode payloads. */ requiredFlags?: string[] publishedAt?: string capturedAt?: string sourceUri?: string sourceCitation?: string body: string[] regions: DocumentRegion[] assetId?: string fileName?: string mimeType?: string fileSize?: number fileType: SourceFileType /** Evidentiary form on the board; independent of MIME/file type. */ captureKind: DocumentCaptureKind metadata: Record } export interface DocumentUploadAnalysis { extractionStatus: 'succeeded' | 'unsupported' | 'failed' matchedFlags: string[] awardedFlags: string[] goals: LevelGoal[] } export interface UploadedCaseDocument extends DocumentExhibit { /** Transient upload response data; it is not part of persisted exhibit state. */ analysis: DocumentUploadAnalysis } export type NotePresentation = 'luggage' | 'lined_sheet' export interface NoteExhibit extends ExhibitBase { type: 'note' content: string presentation: NotePresentation } 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 interface ClaimExhibit extends ExhibitBase { type: 'claim' statement: string } export type Exhibit = FolderExhibit | DocumentExhibit | NoteExhibit | EventExhibit | PartyExhibit | ClaimExhibit 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 LevelGoal { /** Present in author mode so the goal can be edited; omitted in play mode. */ id?: string key: string title: string instructions: string completionMessage: string enabled?: boolean /** Author-only success condition. Expected answer flags stay out of play payloads. */ requiredFlags?: string[] status: 'pending' | 'complete' completedAt?: string /** True only in the response to the mutation that completed this goal. */ newlyCompleted: boolean } export type CaseReportSubmissionStatus = 'evidence_insufficient' | 'evidence_accepted_report_incomplete' | 'accepted' export type EvidenceVerificationStatus = 'accepted' | 'not_evaluated' | 'ocr_unavailable' | 'text_not_matched' | 'semantic_pending' | 'semantic_failed' | 'semantic_rejected' export interface EvidenceVerification { status: EvidenceVerificationStatus detail: string score?: number matchedMarkers?: number requiredMarkers?: number } export interface CaseReportEvidence { connectionId: string documentExhibitId: string displayNumber: number documentTitle: string fileType: SourceFileType relationText: string publishedAt?: string sourceCitation?: string sourceUri?: string evidenceAccepted: boolean verification: EvidenceVerification } export interface CaseReportClaim { claimExhibitId: string statement: string evidence: CaseReportEvidence[] } export interface CaseReport { title: string investigatorName: string requiredForCompletion: boolean status: 'draft' | CaseReportSubmissionStatus feedback?: string issues: string[] claims: CaseReportClaim[] } export interface CaseReportSubmissionInput { investigatorName: string } export interface CaseState { id: string title: string subtitle: string exhibits: Exhibit[] relations: ExhibitRelation[] connections: Connection[] views: BoardView[] viewport: Viewport brief: LevelBrief goals: LevelGoal[] report?: CaseReport 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 sourceLabel?: string sourceUri?: string flagKey: string matcherVersion: 'char_trigram_v1' minimumAnchorMatches: number enabled: boolean anchors: EvidenceMatchAnchorDefinition[] } export interface EvidenceSemanticRuleDefinition { id: string goalId: string goalKey: string name: string targetSubject: string relatedSubject?: string assertion: string successFlagKey: string relatedFlagKey?: string minimumConfidence: number evaluatorVersion: string enabled: boolean } export interface DocumentSemanticAnalysis { status: 'not_needed' | 'unavailable' | 'pending' | 'succeeded' | 'failed' subject?: 'target' | 'related' | 'ambiguous' | 'neither' supportsClaim?: boolean evidenceExcerpt?: string confidence?: number retryable: boolean awardedFlags: string[] goals: LevelGoal[] } 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' } export function isClaimExhibit(exhibit: Exhibit): exhibit is ClaimExhibit { return exhibit.type === 'claim' }