// IIOS Interaction Kernel — P1 schema (native portal only). // Lifted from the Bottom-Up Evolution Atlas §10 kernel DDL. ONLY kernel tables: // no support / inbox / route / AI tables (those are earned in later phases). generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ─── Enums (one value per line) ─────────────────────────────────── enum IiosActorKind { HUMAN AI SERVICE BOT UNKNOWN } enum IiosHandleKind { PORTAL_USER EMAIL PHONE WHATSAPP SLACK MATTERMOST TELEGRAM EXTERNAL_VISITOR } enum IiosInteractionKind { MESSAGE EMAIL SYSTEM_NOTICE INBOX_WORK SUPPORT_CASE MEETING_REQUEST DIGEST SUMMARY NOTIFICATION } enum IiosMessagePartKind { TEXT HTML MARKDOWN MEDIA_REF FILE_REF VOICE_REF LOCATION STRUCTURED_JSON } enum IiosDeliveryState { RECEIVED NORMALIZED POLICY_HELD STORED DELIVERED READ FAILED DELETED_LOCAL REDACTED } enum IiosReceiptKind { DELIVERED READ ACKED FAILED SUPPRESSED RATE_LIMITED } enum IiosInboxItemKind { NEEDS_REPLY NEEDS_REVIEW NEEDS_APPROVAL SUPPORT_UPDATE MEETING_FOLLOWUP DIGEST SYSTEM_ALERT CRM_OWNER_INTEREST } enum IiosInboxState { OPEN SNOOZED DONE ARCHIVED CANCELLED STALE } enum IiosTicketState { NEW OPEN PENDING_CUSTOMER PENDING_INTERNAL RESOLVED CLOSED CANCELLED } enum IiosTicketPriority { P0 P1 P2 P3 P4 } enum IiosCallbackStatus { PENDING SCHEDULED COMPLETED MISSED CANCELLED } enum IiosRouteMode { MANUAL AUTOMATIC HYBRID SIMULATION_ONLY } enum IiosOutputFormat { FORWARD THREADED DIGEST SUMMARY TRANSCRIPT } enum IiosRouteDecisionState { ALLOW DENY REVIEW SUPPRESS SIMULATED } // ─── AI enums (P7) ──────────────────────────────────────────────── enum IiosAiJobType { CLASSIFY SUMMARIZE EXTRACT } enum IiosAiJobStatus { PENDING RUNNING DONE FAILED ABSTAINED } enum IiosAiArtifactType { CLASSIFICATION SUMMARY TRANSCRIPT DIGEST EXTRACTION } enum IiosAiArtifactStatus { PROPOSED ACCEPTED REJECTED SUPERSEDED } enum IiosAiClaimType { MODERATION_FLAG EVENT FAQ INTENT PRIORITY } enum IiosAiClaimValidation { UNVALIDATED ACCEPTED REJECTED } // ─── Kernel tables ──────────────────────────────────────────────── /// Frozen six-vector scope. orgId + appId are REQUIRED (no global-by-null). model IiosScope { id String @id @default(cuid()) orgId String appId String buId String? tenantId String? workspaceId String? projectId String? userScopeId String? createdAt DateTime @default(now()) sourceHandles IiosSourceHandle[] channels IiosChannel[] threads IiosThread[] interactions IiosInteraction[] inboxItems IiosInboxItem[] supportQueues IiosSupportQueue[] tickets IiosTicket[] callbacks IiosCallbackRequest[] @@index([orgId, appId, tenantId]) } /// A channel-specific handle (phone/email/portal user). NOT identity — stays /// UNVERIFIED until MDM resolves it; no silent merge. model IiosSourceHandle { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) kind IiosHandleKind externalId String displayName String? canonicalEntityId String? // set only when MDM resolves; null = unresolved confidence Float @default(0) verificationState String @default("UNVERIFIED") firstSeenAt DateTime @default(now()) lastSeenAt DateTime @default(now()) metadata Json? actors IiosActorRef[] @@unique([scopeId, kind, externalId]) @@index([canonicalEntityId]) } /// The local actor (human / AI / service / unknown) behind an interaction. model IiosActorRef { id String @id @default(cuid()) kind IiosActorKind sourceHandleId String? sourceHandle IiosSourceHandle? @relation(fields: [sourceHandleId], references: [id]) platformPrincipalRef String? canonicalEntityId String? displayName String? createdAt DateTime @default(now()) threadsCreated IiosThread[] @relation("ThreadCreatedBy") participations IiosThreadParticipant[] interactions IiosInteraction[] receipts IiosMessageReceipt[] unreadCounters IiosUnreadCounter[] inboxItemsOwned IiosInboxItem[] teamMemberships IiosSupportTeamMember[] ticketsRequested IiosTicket[] @relation("TicketRequester") ticketsAssigned IiosTicket[] @relation("TicketAssignee") callbacksRequested IiosCallbackRequest[] } /// A configured channel surface (PORTAL in P1). model IiosChannel { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) channelType String channelName String direction String @default("BOTH") capabilityContract Json? externalRef String? enabled Boolean @default(true) createdAt DateTime @default(now()) threads IiosThread[] interactions IiosInteraction[] @@index([scopeId, channelType]) } /// A conversation/thread on a channel. model IiosThread { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) channelId String? channel IiosChannel? @relation(fields: [channelId], references: [id]) threadKind String @default("CONVERSATION") subject String? externalThreadRef String? status String @default("OPEN") visibilityClass String @default("PRIVATE") createdByActorId String? createdByActor IiosActorRef? @relation("ThreadCreatedBy", fields: [createdByActorId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt metadata Json? participants IiosThreadParticipant[] interactions IiosInteraction[] unreadCounters IiosUnreadCounter[] inboxItems IiosInboxItem[] ticketLinks IiosTicketThreadLink[] @@index([scopeId, status]) } model IiosThreadParticipant { threadId String thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade) actorId String actor IiosActorRef @relation(fields: [actorId], references: [id]) participantRole String @default("MEMBER") joinedAt DateTime @default(now()) leftAt DateTime? @@id([threadId, actorId]) } /// The semantic envelope. Idempotent per (scope, idempotencyKey). model IiosInteraction { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) kind IiosInteractionKind threadId String? thread IiosThread? @relation(fields: [threadId], references: [id]) channelId String? channel IiosChannel? @relation(fields: [channelId], references: [id]) actorId String? actor IiosActorRef? @relation(fields: [actorId], references: [id]) parentInteractionId String? parent IiosInteraction? @relation("InteractionParent", fields: [parentInteractionId], references: [id]) children IiosInteraction[] @relation("InteractionParent") externalId String? idempotencyKey String occurredAt DateTime @default(now()) receivedAt DateTime @default(now()) status IiosDeliveryState @default(RECEIVED) policyDecisionRef String? consentReceiptRef String? crreBundleRef String? traceId String? metadata Json? parts IiosMessagePart[] receipts IiosMessageReceipt[] inboxItems IiosInboxItem[] ticketsCreatedFrom IiosTicket[] @@unique([scopeId, idempotencyKey]) @@index([threadId, occurredAt]) } model IiosMessagePart { id String @id @default(cuid()) interactionId String interaction IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade) partIndex Int kind IiosMessagePartKind bodyText String? contentRef String? mimeType String? sizeBytes BigInt? checksumSha256 String? metadata Json? @@unique([interactionId, partIndex]) } /// Transactional outbox — written in the same tx as the business row. model IiosOutboxEvent { eventId String @id @default(cuid()) aggregateType String aggregateId String eventType String cloudEvent Json partitionKey String status String @default("PENDING") attempts Int @default(0) nextAttemptAt DateTime @default(now()) createdAt DateTime @default(now()) publishedAt DateTime? @@index([status, nextAttemptAt, createdAt]) } /// Idempotent-consumer ledger for the outbox relay / downstream consumers. model IiosProcessedEvent { consumerName String eventId String processedAt DateTime @default(now()) resultHash String? @@id([consumerName, eventId]) } // ─── P2: messaging — receipts & unread ──────────────────────────── /// Delivery/read receipt for a message interaction, per actor. model IiosMessageReceipt { id String @id @default(cuid()) interactionId String interaction IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade) actorId String? actor IiosActorRef? @relation(fields: [actorId], references: [id]) receiptKind IiosReceiptKind occurredAt DateTime @default(now()) providerReceiptRef String? metadata Json? @@unique([interactionId, actorId, receiptKind]) @@index([interactionId]) } /// Per-(thread, actor) unread count + last-read marker. model IiosUnreadCounter { threadId String thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade) actorId String actor IiosActorRef @relation(fields: [actorId], references: [id]) unreadCount Int @default(0) lastReadInteractionId String? updatedAt DateTime @updatedAt @@id([threadId, actorId]) } // ─── P3: Inbox (work/awareness surface) ─────────────────────────── /// A durable action/awareness item owned by one actor. Fed by the event /// projector (deterministic, collapse-per-thread). NOT a thin unread projection. model IiosInboxItem { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) ownerActorId String ownerActor IiosActorRef @relation(fields: [ownerActorId], references: [id]) kind IiosInboxItemKind state IiosInboxState @default(OPEN) title String summary String? priority String @default("NORMAL") dueAt DateTime? sourceInteractionId String? sourceInteraction IiosInteraction? @relation(fields: [sourceInteractionId], references: [id]) threadId String? thread IiosThread? @relation(fields: [threadId], references: [id]) traceId String? actionRef Json? policyDecisionRef String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt metadata Json? stateHistory IiosInboxItemStateHistory[] @@index([ownerActorId, state, priority]) } /// Audit trail of inbox-item state transitions. model IiosInboxItemStateHistory { id String @id @default(cuid()) inboxItemId String inboxItem IiosInboxItem @relation(fields: [inboxItemId], references: [id], onDelete: Cascade) fromState IiosInboxState? toState IiosInboxState actorId String? reasonCode String? at DateTime @default(now()) @@index([inboxItemId]) } // ─── P4: Support (specialization over message + inbox) ──────────── /// A support queue; agents (team members) are assigned tickets from it. model IiosSupportQueue { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) name String supportTier String @default("REGULAR") slaPolicyRef String? // SLA placeholder (no timer logic until P7) assignmentPolicy Json? enabled Boolean @default(true) createdAt DateTime @default(now()) members IiosSupportTeamMember[] tickets IiosTicket[] @@index([scopeId]) } /// An agent's membership + capacity in a queue. Agent = an ActorRef that is a member. model IiosSupportTeamMember { queueId String queue IiosSupportQueue @relation(fields: [queueId], references: [id], onDelete: Cascade) actorId String actor IiosActorRef @relation(fields: [actorId], references: [id]) role String @default("AGENT") maxActive Int @default(3) activeCount Int @default(0) availabilityState String @default("OFFLINE") @@id([queueId, actorId]) @@index([actorId]) } /// Support work object. Linked many-to-many to threads; the thread stays generic. model IiosTicket { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) queueId String? queue IiosSupportQueue? @relation(fields: [queueId], references: [id]) requesterActorId String? requesterActor IiosActorRef? @relation("TicketRequester", fields: [requesterActorId], references: [id]) assignedActorId String? assignedActor IiosActorRef? @relation("TicketAssignee", fields: [assignedActorId], references: [id]) subject String state IiosTicketState @default(NEW) priority IiosTicketPriority @default(P3) issueClass String? issueSubclass String? slaDueAt DateTime? // SLA placeholder (no timer logic until P7) createdFromInteractionId String? createdFromInteraction IiosInteraction? @relation(fields: [createdFromInteractionId], references: [id]) traceId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt metadata Json? threadLinks IiosTicketThreadLink[] stateHistory IiosTicketStateHistory[] callbacks IiosCallbackRequest[] @@index([scopeId, state]) @@index([assignedActorId, state]) } /// M:N link between tickets and kernel threads (one thread ↔ many tickets, and vice-versa). model IiosTicketThreadLink { ticketId String ticket IiosTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade) threadId String thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade) relationKind String @default("PRIMARY") linkedAt DateTime @default(now()) @@id([ticketId, threadId]) @@index([threadId]) } /// Ticket state-transition audit. model IiosTicketStateHistory { id String @id @default(cuid()) ticketId String ticket IiosTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade) fromState IiosTicketState? toState IiosTicketState actorId String? reasonCode String? at DateTime @default(now()) @@index([ticketId]) } /// Callback request placeholder (the Meeting module is P8; meetingRef is a stub). model IiosCallbackRequest { id String @id @default(cuid()) scopeId String scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade) ticketId String? ticket IiosTicket? @relation(fields: [ticketId], references: [id]) requesterActorId String requesterActor IiosActorRef @relation(fields: [requesterActorId], references: [id]) preferChannel String preferredTime DateTime? notes String? status IiosCallbackStatus @default(PENDING) meetingRef String? createdAt DateTime @default(now()) @@index([scopeId, status]) } // ─── P5: Adapters (anti-corruption layer, simulation mode) ──────── /// Raw inbound provider payload (claim-check / replay). Verified before storing. model IiosInboundRawEvent { id String @id @default(cuid()) channelType String externalEventId String? signatureStatus String // VERIFIED | INVALID | UNSIGNED status String @default("RECEIVED") // RECEIVED | NORMALIZED | REJECTED | FAILED headers Json? payload Json traceId String? interactionId String? createdAt DateTime @default(now()) @@unique([channelType, externalEventId]) @@index([status]) } /// A signed command to send on an external channel — executed by the sandbox sink. model IiosOutboundCommand { id String @id @default(cuid()) channelType String target String payload Json status String @default("PENDING") // PENDING | SENT | FAILED | RATE_LIMITED idempotencyKey String @unique providerRef String? createdAt DateTime @default(now()) attempts IiosDeliveryAttempt[] @@index([channelType, status]) } model IiosDeliveryAttempt { id String @id @default(cuid()) commandId String command IiosOutboundCommand @relation(fields: [commandId], references: [id], onDelete: Cascade) attemptNo Int status String providerRef String? latencyMs Int? errorCode String? at DateTime @default(now()) @@index([commandId]) } /// Per-(channelType, key) token/window bucket for outbound throttling. model IiosRateLimitBucket { channelType String bucketKey String windowStart DateTime @default(now()) count Int @default(0) resetAt DateTime @@id([channelType, bucketKey]) } // ─── P6: Routing (preview-first community forwarding) ────────────── /// One origin→destination route rule. Preview-first: SIMULATION_ONLY + disabled /// + requiresReview by default. Restricted destinations are deny-by-default. model IiosRouteBinding { id String @id @default(cuid()) scopeId String originChannelType String originRef String? destinationChannelType String destinationRef String? restrictionProfile String? // e.g. CHILD / SENIORS / ADULT / PUBLIC mode IiosRouteMode @default(SIMULATION_ONLY) outputFormat IiosOutputFormat @default(FORWARD) frequency String @default("IMMEDIATE") includeAttachments Boolean @default(false) requiresReview Boolean @default(true) rulepackVersion String @default("v1") enabled Boolean @default(false) createdAt DateTime @default(now()) decisions IiosRouteDecision[] @@index([scopeId, originChannelType]) } /// Per-(interaction, binding) routing decision + rendered preview. No send during simulation. model IiosRouteDecision { id String @id @default(cuid()) interactionId String routeBindingId String routeBinding IiosRouteBinding @relation(fields: [routeBindingId], references: [id], onDelete: Cascade) decisionState IiosRouteDecisionState reasonCodes String[] previewPayload Json outputFormat IiosOutputFormat rulepackVersion String simulationId String? decidedByActorId String? executed Boolean @default(false) executedCommandId String? createdAt DateTime @default(now()) @@unique([interactionId, routeBindingId]) @@index([decisionState]) } /// A deterministic (RULE) or later AI-proposed flag on an interaction. AI can /// propose but never allow a route (P7). model IiosModerationFlag { id String @id @default(cuid()) interactionId String flagType String severity String @default("LOW") proposedBy String @default("RULE") confidence Float? explanation String? createdAt DateTime @default(now()) resolvedAt DateTime? @@index([interactionId]) } // ─── AI Interaction SDK tables (P7) — proposal layer, never decides ───────── /// A scoped, purposed AI work order (classify / summarize / extract). Carries the /// provenance the critics mandate (input_scope_hash, policy_decision_id, purpose, /// replay_version, traceId, idempotencyKey). Idempotent per key. model IiosAiJob { id String @id @default(cuid()) scopeId String jobType IiosAiJobType interactionId String? inputWindowRef String purpose String status IiosAiJobStatus @default(PENDING) modelPolicyRef String @default("golden-v1") policyDecisionRef String? inputScopeHash String replayVersion String @default("v1") abstentionReason String? traceId String? idempotencyKey String @unique createdAt DateTime @default(now()) artifacts IiosAiArtifact[] @@index([scopeId, jobType]) @@index([interactionId]) } /// Model/prompt/tool execution metadata + cost. The budget governor (KG-12) sums /// costUnits per scope. In P7 provider='fake' (deterministic golden model). model IiosAiModelRun { id String @id @default(cuid()) scopeId String provider String @default("fake") model String modelVersion String promptVersion String toolPolicyRef String? tokensIn Int @default(0) tokensOut Int @default(0) costUnits Int @default(0) latencyMs Int @default(0) createdAt DateTime @default(now()) artifacts IiosAiArtifact[] toolCalls IiosAiToolCall[] @@index([scopeId]) } /// A durable AI proposal/output. NEVER committed truth by itself — status starts /// PROPOSED and only a human accept (or a deterministic gate) advances it. model IiosAiArtifact { id String @id @default(cuid()) jobId String job IiosAiJob @relation(fields: [jobId], references: [id], onDelete: Cascade) modelRunId String modelRun IiosAiModelRun @relation(fields: [modelRunId], references: [id], onDelete: Cascade) artifactType IiosAiArtifactType status IiosAiArtifactStatus @default(PROPOSED) confidence Float? contentRef Json explanationRef String? abstentionReason String? acceptedByActorId String? createdAt DateTime @default(now()) claims IiosAiClaim[] evidence IiosAiEvidenceLink[] @@index([jobId]) @@index([status]) } /// A structured extracted claim (event date, intent, priority, FAQ, moderation /// flag). validationStatus stays UNVALIDATED until a human/deterministic accept. model IiosAiClaim { id String @id @default(cuid()) artifactId String artifact IiosAiArtifact @relation(fields: [artifactId], references: [id], onDelete: Cascade) claimType IiosAiClaimType claimJson Json confidence Float? validationStatus IiosAiClaimValidation @default(UNVALIDATED) acceptedByRef String? createdAt DateTime @default(now()) @@index([artifactId]) } /// Source citation for an AI claim/artifact (message / segment / KB chunk). model IiosAiEvidenceLink { id String @id @default(cuid()) artifactId String artifact IiosAiArtifact @relation(fields: [artifactId], references: [id], onDelete: Cascade) sourceType String sourceId String spanRef String? relevanceScore Float? createdAt DateTime @default(now()) @@index([artifactId]) } /// Tool calls made by an agent under MCP/tool policy. Schema now; exercised when a /// real provider + tools land in P9. model IiosAiToolCall { id String @id @default(cuid()) modelRunId String modelRun IiosAiModelRun @relation(fields: [modelRunId], references: [id], onDelete: Cascade) toolName String toolInputHash String toolOutputRef String? policyRefId String? status String @default("PENDING") createdAt DateTime @default(now()) @@index([modelRunId]) } /// Reference to a vector embedding record/index; does not expose text. Schema now; /// exercised when RAG lands in P9. model IiosEmbeddingRef { id String @id @default(cuid()) sourceType String sourceId String embeddingModel String vectorStoreRef String embeddingHash String version String createdAt DateTime @default(now()) @@unique([sourceType, sourceId, version]) }