feat(service): NestJS kernel skeleton + 10-table kernel schema (P1.1+1.2)

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>
This commit is contained in:
2026-06-30 21:16:31 +05:30
parent c73acce0e8
commit 9cfd1ab1ab
12 changed files with 3743 additions and 10 deletions
+257
View File
@@ -0,0 +1,257 @@
// 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])
}