Files
iios/packages/iios-service/prisma/schema.prisma
T
maaz519 f3c4ba72b5 feat(iios): generic interaction-annotation primitive (backs emoji reactions)
Adds IiosInteractionAnnotation — an actor attaches an OPAQUE (annotationType, value)
label to an interaction. The kernel stores + aggregates them but never interprets the
strings (the chat app writes type "reaction" / value = emoji); the same primitive backs
pins/saves/flags/tags later. No chat vocabulary in kernel code — verified by grep.

- MessageService.toggleAnnotation(): governed (participant-only via new OPA rule
  iios.interaction.annotate), idempotent toggle keyed on
  (scope, interaction, actor, type, value); history DTO carries aggregated
  { type, value, users[] } groups.
- Gateway: `annotate` event → broadcasts `annotation` (refreshed user list) to the room.
- DevOpaPort: annotate allowed only for thread members (real OPA can tighten later).
- Migration add_interaction_annotations (additive; dev data untouched).

Tests: 178 pass (+3: toggle/aggregate, coexisting values, non-member denied).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:20:47 +05:30

1301 lines
40 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
}
// ─── Calendar / Meeting enums (P8) ────────────────────────────────
enum IiosMeetingType {
ZOOM
PHONE
IN_PERSON
CALLBACK
INTERNAL
}
enum IiosMeetingStatus {
REQUESTED
SCHEDULED
CONFIRMED
CANCELLED
COMPLETED
NO_SHOW
}
enum IiosMeetingRequestStatus {
PENDING
PROPOSED
ACCEPTED
DECLINED
SCHEDULED
CANCELLED
}
enum IiosParticipantRole {
ORGANIZER
REQUIRED
OPTIONAL
}
enum IiosAttendanceStatus {
INVITED
ACCEPTED
DECLINED
TENTATIVE
ATTENDED
NO_SHOW
}
enum IiosParticipantVisibility {
FULL
LIMITED
NONE
}
enum IiosConsentStatus {
UNKNOWN
GRANTED
DENIED
REVOKED
}
enum IiosTranscriptStatus {
PENDING
READY
BLOCKED
}
enum IiosSummaryStatus {
PROPOSED
APPROVED
PUBLISHED
REJECTED
}
enum IiosActionItemStatus {
OPEN
IN_PROGRESS
DONE
CANCELLED
}
enum IiosCalendarProviderType {
SIMULATED
GOOGLE
OUTLOOK
ICS
}
enum IiosCalendarSyncStatus {
IDLE
SYNCING
ERROR
}
enum IiosCalendarEventStatus {
CONFIRMED
TENTATIVE
CANCELLED
}
// ─── 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?
cellId String @default("cell-default") // P9 cell-partition hook
createdAt DateTime @default(now())
sourceHandles IiosSourceHandle[]
channels IiosChannel[]
threads IiosThread[]
interactions IiosInteraction[]
annotations IiosInteractionAnnotation[]
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?
redactedAt DateTime? // set when this subject has been erased (DSR, P9)
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[]
annotations IiosInteractionAnnotation[]
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)
dataClass String @default("internal") // envelope data class; drives retention window (P9)
policyDecisionRef String?
consentReceiptRef String?
crreBundleRef String?
traceId String?
metadata Json?
parts IiosMessagePart[]
receipts IiosMessageReceipt[]
annotations IiosInteractionAnnotation[]
inboxItems IiosInboxItem[]
ticketsCreatedFrom IiosTicket[]
@@unique([scopeId, idempotencyKey])
@@index([threadId, occurredAt])
}
/// A generic annotation an actor attaches to an interaction. Both `annotationType`
/// and `value` are OPAQUE, app-supplied strings the kernel stores and aggregates
/// but never interprets — e.g. the chat app writes type "reaction" / value "👍".
/// The same primitive backs pins, saves, flags, tags later. No chat vocabulary here.
model IiosInteractionAnnotation {
id String @id @default(cuid())
scopeId String
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
targetInteractionId String
target IiosInteraction @relation(fields: [targetInteractionId], references: [id], onDelete: Cascade)
actorId String
actor IiosActorRef @relation(fields: [actorId], references: [id])
annotationType String
value String
createdAt DateTime @default(now())
@@unique([scopeId, targetInteractionId, actorId, annotationType, value])
@@index([targetInteractionId])
}
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])
}
/// Per-resource retention snapshot (P9). Absolute lifecycle timestamps frozen from the
/// resource's age + policy window; read by the retention sweep. delete = redact-in-place.
model IiosRetentionPolicySnapshot {
id String @id @default(cuid())
policyKey String
scopeSnapshotId String
targetType String // 'interaction'
targetId String
dataClass String
retainUntil DateTime?
archiveAfter DateTime
deleteAfter DateTime
sourceVersion String @default("v1")
status String @default("ACTIVE") // ACTIVE | ARCHIVED | REDACTED
createdAt DateTime @default(now())
@@unique([targetType, targetId])
@@index([status, deleteAfter])
@@index([scopeSnapshotId, status])
}
/// Legal/compliance hold (P9 DSR). An ACTIVE, unexpired hold over a target BLOCKS
/// erasure/retention deletion until an authorised release. Never auto-released.
model IiosComplianceHold {
id String @id @default(cuid())
scopeId String
targetType String // 'data_subject' | 'message' | 'media'
targetId String
holdReason String
appliedByActorId String?
status String @default("ACTIVE") // ACTIVE | RELEASED
createdAt DateTime @default(now())
expiresAt DateTime?
releasedAt DateTime?
@@index([scopeId, targetType, targetId, status])
}
/// Projection replay cursor (P9, KG-06). Ordered high-water-mark + rolling checksum per
/// (projection, topic, partition) proving replay reproduces the same projection outcome.
model IiosProjectionCursor {
id String @id @default(cuid())
projectionName String
sourceTopic String
partitionKey String
lastEventId String
lastOffset Int @default(0)
checksum String
updatedAt DateTime @updatedAt
@@unique([projectionName, sourceTopic, partitionKey])
@@index([partitionKey])
}
/// Idempotent-command ledger (P9). Suppresses duplicate externally-visible mutations:
/// same (scope, command, key) replays the cached response; a different request → conflict.
model IiosIdempotencyCommand {
id String @id @default(cuid())
scopeId String?
commandName String
idempotencyKey String
schemaVersion String @default("v1")
actorRefId String?
requestHash String
responseHash String?
response Json?
status String @default("IN_FLIGHT") // IN_FLIGHT | COMPLETED | FAILED
createdAt DateTime @default(now())
completedAt DateTime?
@@unique([scopeId, commandName, idempotencyKey])
@@index([scopeId, status])
}
// ─── 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 | BLOCKED
scopeId String? // owning tenant scope (P9 tenant fencing + per-tenant quota)
idempotencyKey String @unique
providerRef String?
consentReceiptRef String? // CMP consent receipt this send went out under (P9)
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])
}
// ─── Calendar / Meeting SDK tables (P8) — interaction specialization ────────
/// A request to schedule/reschedule/cancel a meeting. Genesis: a P4 callback
/// (callbackRequestId), an accepted P7 AI EVENT claim (sourceClaimId), or direct.
model IiosMeetingRequest {
id String @id @default(cuid())
scopeId String
interactionId String?
requestedByActorId String
targetActorId String?
requestedWindowJson Json
meetingType IiosMeetingType @default(CALLBACK)
status IiosMeetingRequestStatus @default(PENDING)
resultingMeetingId String?
callbackRequestId String?
sourceClaimId String?
createdAt DateTime @default(now())
@@index([scopeId, status])
}
/// A meeting: Zoom / phone / in-person / callback / internal. Owns calendar
/// semantics; a message is only a related artifact.
model IiosMeeting {
id String @id @default(cuid())
scopeId String
interactionId String?
calendarEventId String?
meetingType IiosMeetingType
providerRef String?
joinUrlRef String?
status IiosMeetingStatus @default(REQUESTED)
organizerActorId String
threadId String?
title String
startAt DateTime?
endAt DateTime?
timezone String @default("UTC")
createdAt DateTime @default(now())
participants IiosMeetingParticipant[]
transcripts IiosMeetingTranscript[]
summaries IiosMeetingSummary[]
actionItems IiosMeetingActionItem[]
@@index([scopeId, status])
}
/// Attendee with role, attendance, per-attendee recording consent + visibility.
/// The consent + visibility model is the P8 safety spine (KG-14 / Scenario 6).
model IiosMeetingParticipant {
id String @id @default(cuid())
meetingId String
meeting IiosMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
actorRefId String?
sourceHandleId String?
role IiosParticipantRole @default(REQUIRED)
attendanceStatus IiosAttendanceStatus @default(INVITED)
recordingConsent IiosConsentStatus @default(UNKNOWN)
visibility IiosParticipantVisibility @default(FULL)
createdAt DateTime @default(now())
@@unique([meetingId, actorRefId])
@@index([meetingId])
}
/// Transcript artifact metadata. Created only after the consent gate passes;
/// otherwise status=BLOCKED with no segments.
model IiosMeetingTranscript {
id String @id @default(cuid())
meetingId String
meeting IiosMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
sourceType String @default("SIMULATED")
transcriptRef String
consentRefId String?
status IiosTranscriptStatus @default(PENDING)
language String @default("en")
createdAt DateTime @default(now())
segments IiosTranscriptSegment[]
@@index([meetingId])
}
/// Speaker-attributed transcript segment.
model IiosTranscriptSegment {
id String @id @default(cuid())
transcriptId String
transcript IiosMeetingTranscript @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
segmentNo Int
speakerActorId String?
startMs Int
endMs Int
contentRef String
confidence Float?
createdAt DateTime @default(now())
@@index([transcriptId])
}
/// AI or human meeting summary; links to a P7 ai_artifact.
model IiosMeetingSummary {
id String @id @default(cuid())
meetingId String
meeting IiosMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
aiArtifactId String?
summaryType String @default("AI")
status IiosSummaryStatus @default(PROPOSED)
approvedByActorId String?
publishedAt DateTime?
createdAt DateTime @default(now())
@@index([meetingId])
}
/// Generic task/follow-up (Inbox-owned source of truth), derived from meetings,
/// support, messages or CRM workflows.
model IiosActionItem {
id String @id @default(cuid())
scopeId String
sourceInteractionId String?
sourceMessageId String?
ownerActorId String?
ownerTeamId String?
title String
status IiosActionItemStatus @default(OPEN)
dueAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
meetingLinks IiosMeetingActionItem[]
@@index([scopeId, status])
}
/// Meeting-derived action item linked to a generic action_item.
model IiosMeetingActionItem {
id String @id @default(cuid())
meetingId String
meeting IiosMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
actionItemId String
actionItem IiosActionItem @relation(fields: [actionItemId], references: [id], onDelete: Cascade)
ownerActorId String?
sourceSegmentId String?
status IiosActionItemStatus @default(OPEN)
createdAt DateTime @default(now())
@@unique([meetingId, actionItemId])
@@index([meetingId])
}
/// Connected calendar/provider account or service mailbox; stores refs, not secrets.
model IiosCalendarProviderAccount {
id String @id @default(cuid())
scopeId String
actorRefId String?
providerType IiosCalendarProviderType @default(SIMULATED)
accountHandleToken String
secretRef String?
syncStatus IiosCalendarSyncStatus @default(IDLE)
createdAt DateTime @default(now())
events IiosCalendarEvent[]
cursors IiosCalendarSyncCursor[]
@@index([scopeId])
}
/// Imported or created calendar event with policy-scoped metadata.
model IiosCalendarEvent {
id String @id @default(cuid())
scopeId String
providerAccountId String?
providerAccount IiosCalendarProviderAccount? @relation(fields: [providerAccountId], references: [id], onDelete: Cascade)
externalEventId String?
title String
startAt DateTime
endAt DateTime
status IiosCalendarEventStatus @default(CONFIRMED)
visibilityClass String @default("internal")
interactionId String?
createdAt DateTime @default(now())
@@unique([providerAccountId, externalEventId])
@@index([scopeId])
}
/// Availability / scheduling constraints for an actor or team.
model IiosAvailabilityWindow {
id String @id @default(cuid())
scopeId String
actorRefId String?
teamProjectionId String?
startAt DateTime
endAt DateTime
source String @default("USER")
recurrenceJson Json?
status String @default("ACTIVE")
createdAt DateTime @default(now())
@@index([scopeId, actorRefId])
}
/// Incremental sync cursor / replay position for a provider calendar.
model IiosCalendarSyncCursor {
id String @id @default(cuid())
providerAccountId String
providerAccount IiosCalendarProviderAccount @relation(fields: [providerAccountId], references: [id], onDelete: Cascade)
externalCursor String?
lastSyncAt DateTime?
status IiosCalendarSyncStatus @default(IDLE)
errorCode String?
createdAt DateTime @default(now())
@@unique([providerAccountId])
}
// ─── Observability (P9) ───────────────────────────────────────────
/// Binds an IIOS action to its trace / actor / resource for cross-service causality
/// and "replay a decision" (mandated iios_audit_link). Written at decision points.
model IiosAuditLink {
id String @id @default(cuid())
traceId String?
correlationId String?
scopeId String?
actorRefId String?
action String
resourceType String
resourceId String
createdAt DateTime @default(now())
@@index([traceId])
@@index([resourceType, resourceId])
@@index([scopeId])
}
/// Dead-letter / replay item (P9, mandated dlq_item). A consumer/relay failure is
/// quarantined here (never silently swallowed — KG-13) and can be replayed by ops.
model IiosDlqItem {
id String @id @default(cuid())
sourceType String // 'projection' | 'outbox' | 'outbound' | ...
sourceId String
failureStage String
errorCode String?
retryCount Int @default(0)
payloadRef String? // the outbox eventId to re-dispatch
consumerName String?
scopeId String? // tenant quarantine
status String @default("QUARANTINED") // QUARANTINED | RESOLVED | ARCHIVED
createdAt DateTime @default(now())
lastFailedAt DateTime @default(now())
resolvedAt DateTime?
@@unique([consumerName, sourceId])
@@index([scopeId, status])
}