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>
This commit is contained in:
@@ -32,9 +32,17 @@ export class RouteSimulator {
|
||||
|
||||
const text = interaction.parts.find((p) => p.kind === 'TEXT')?.bodyText ?? '';
|
||||
const hasAttachment = interaction.parts.some((p) => !!p.contentRef);
|
||||
const flags = this.rules.flag(text);
|
||||
const flagTypes = flags.map((f) => f.flagType);
|
||||
await this.persistFlags(interactionId, flags);
|
||||
const ruleFlags = this.rules.flag(text);
|
||||
await this.persistFlags(interactionId, ruleFlags);
|
||||
|
||||
// Union deterministic RULE flags with any persisted flags — including
|
||||
// proposedBy:'AI' from the P7 layer. AI flags therefore influence routing
|
||||
// exactly like rule flags: they can only push toward REVIEW/DENY (KG-05).
|
||||
const persisted = await this.prisma.iiosModerationFlag.findMany({ where: { interactionId } });
|
||||
const flagTypes = Array.from(new Set([...ruleFlags.map((f) => f.flagType), ...persisted.map((p) => p.flagType)]));
|
||||
|
||||
// A P7 AI summary (if one was proposed) renders into SUMMARY/TRANSCRIPT/DIGEST previews.
|
||||
const aiSummary = await this.latestAiSummary(interactionId);
|
||||
|
||||
const simulationId = opts?.simulationId ?? randomUUID();
|
||||
const bindings = await this.prisma.iiosRouteBinding.findMany({
|
||||
@@ -49,7 +57,7 @@ export class RouteSimulator {
|
||||
const decisions = [];
|
||||
for (const b of bindings) {
|
||||
const { state, reasonCodes } = this.rules.decide(b, flagTypes);
|
||||
const previewPayload = this.render(b, text, hasAttachment);
|
||||
const previewPayload = this.render(b, text, hasAttachment, aiSummary);
|
||||
const decision = await this.prisma.iiosRouteDecision.upsert({
|
||||
where: { interactionId_routeBindingId: { interactionId, routeBindingId: b.id } },
|
||||
create: {
|
||||
@@ -94,24 +102,36 @@ export class RouteSimulator {
|
||||
return { simulationId, decisions };
|
||||
}
|
||||
|
||||
private render(binding: IiosRouteBinding, text: string, hasAttachment: boolean) {
|
||||
private render(binding: IiosRouteBinding, text: string, hasAttachment: boolean, aiSummary?: string) {
|
||||
const attach = binding.includeAttachments && hasAttachment ? ' [+attachment]' : '';
|
||||
// SUMMARY/TRANSCRIPT/DIGEST use the P7 AI summary if one was proposed; else the
|
||||
// P6 deterministic stub. FORWARD/THREADED are verbatim. Still preview-only — no send.
|
||||
const rendered =
|
||||
binding.outputFormat === 'DIGEST'
|
||||
? `[digest] ${text}`
|
||||
? `[digest] ${aiSummary ?? text}`
|
||||
: binding.outputFormat === 'SUMMARY'
|
||||
? `[summary] ${text.slice(0, 60)}` // deterministic stub; real summary is P7 (AI)
|
||||
? (aiSummary ?? `[summary] ${text.slice(0, 60)}`)
|
||||
: binding.outputFormat === 'TRANSCRIPT'
|
||||
? `[transcript] ${text}`
|
||||
? `[transcript] ${aiSummary ?? text}`
|
||||
: text; // FORWARD / THREADED
|
||||
return {
|
||||
format: binding.outputFormat,
|
||||
destinationChannelType: binding.destinationChannelType,
|
||||
destinationRef: binding.destinationRef,
|
||||
aiSourced: !!aiSummary && binding.outputFormat !== 'FORWARD' && binding.outputFormat !== 'THREADED',
|
||||
text: rendered + attach,
|
||||
};
|
||||
}
|
||||
|
||||
private async latestAiSummary(interactionId: string): Promise<string | undefined> {
|
||||
const artifact = await this.prisma.iiosAiArtifact.findFirst({
|
||||
where: { artifactType: 'SUMMARY', job: { interactionId } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
const content = artifact?.contentRef as { text?: string } | undefined;
|
||||
return content?.text;
|
||||
}
|
||||
|
||||
private async persistFlags(interactionId: string, flags: RuleFlagLike[]): Promise<void> {
|
||||
if (flags.length === 0) return;
|
||||
const existing = await this.prisma.iiosModerationFlag.count({ where: { interactionId, proposedBy: 'RULE' } });
|
||||
|
||||
Reference in New Issue
Block a user