9cfd1ab1ab
Reuses support-service bootstrap/health/prisma patterns. Kernel-only Prisma
schema (scope/source_handle/actor/channel/thread/participant/interaction/
message_part/outbox/processed_event) with orgId/appId NOT NULL and
@@unique([scopeId, idempotencyKey]). Health verified: {status:ok,db:true}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
258 lines
7.6 KiB
Plaintext
258 lines
7.6 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
|
|
}
|
|
|
|
// ─── 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[]
|
|
|
|
@@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[]
|
|
}
|
|
|
|
/// 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[]
|
|
|
|
@@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[]
|
|
|
|
@@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])
|
|
}
|