Files
iios/packages/iios-service/src/ai/ai.projector.ts
T
maaz519 68d922c248 feat(p7): wire AI into P6 routing + AiProjector (KG-05 structural proof)
Task 7.4: route-simulator now unions RULE flags with persisted AI moderation
flags before decide() — so AI flags influence routing exactly like rule flags,
only ever REVIEW/DENY, never ALLOW. SUMMARY/TRANSCRIPT/DIGEST previews render a
proposed AI summary (aiSourced flag) instead of the P6 stub, still preview-only.
AiProjector auto-runs CLASSIFY on interaction.normalized (channel-bearing only),
idempotent via claim(). 4 tests prove KG-05: AI flag forces REVIEW on an
auto-ALLOW binding, DENY on CHILD, AI summary in preview with 0 sends, projector
idempotency. Full suite 79 green.

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

47 lines
1.8 KiB
TypeScript

import { Injectable, OnModuleInit } from '@nestjs/common';
import { CloudEvent, IIOS_EVENTS } from '@insignia/iios-contracts';
import { PrismaService } from '../prisma/prisma.service';
import { OutboxBus } from '../outbox/outbox.bus';
import { AiJobService } from './ai.service';
/**
* Auto-proposal (P7). Reacts to `interaction.normalized` — channel-bearing messages
* that arrived via an adapter — and runs a CLASSIFY job so inbound content gets an AI
* moderation proposal automatically. Those flags only ever push routing toward
* REVIEW/DENY (KG-05); nothing is sent or approved. Idempotent via `claim()` and the
* job's own idempotency key.
*/
@Injectable()
export class AiProjector implements OnModuleInit {
private readonly consumer = 'ai-projector';
constructor(
private readonly prisma: PrismaService,
private readonly bus: OutboxBus,
private readonly ai: AiJobService,
) {}
onModuleInit(): void {
this.bus.on(IIOS_EVENTS.interactionNormalized, (p) => void this.onNormalized(p as CloudEvent).catch(() => {}));
}
async onNormalized(event: CloudEvent): Promise<void> {
if (!(await this.claim(event.id))) return;
const { interactionId } = event.data as { interactionId: string };
const interaction = await this.prisma.iiosInteraction.findUnique({
where: { id: interactionId },
include: { channel: true },
});
if (!interaction?.channel) return; // native DMs have no origin channel → not auto-classified
await this.ai.runJob({ interactionId, jobType: 'CLASSIFY' });
}
private async claim(eventId: string): Promise<boolean> {
const res = await this.prisma.iiosProcessedEvent.createMany({
data: [{ consumerName: this.consumer, eventId }],
skipDuplicates: true,
});
return res.count > 0;
}
}