Files
iios/packages/iios-service/prisma/schema.prisma
T
maaz519 42bb99c09f feat(p7): AI tables + enums + migration + events + InferencePort contract
Task 7.1: 7 AI-proposal-layer tables (IiosAiJob, IiosAiModelRun, IiosAiArtifact,
IiosAiClaim, IiosAiEvidenceLink, IiosAiToolCall, IiosEmbeddingRef) + enums;
migration ai-init. Adds ai.job.created / ai.artifact.proposed / ai.artifact.accepted
events and a standalone InferencePort contract (not part of sealed IiosPlatformPorts).
resetDb truncates the new tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:26:21 +05:30

846 lines
26 KiB
Plaintext

// 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])
}