feat(gmw): moderation explainability + semantic message search

- Persist structured verdict (flags/severity/confidence/evidence) on
  moderation_actions so the public web can show WHY a message was moderated.
- Add a persistent Qdrant archive collection (gmw_message_archive); embed
  every captured message at capture time (fire-and-forget, best-effort).
- Public semantic search over the archive (backend oRPC + FE toggle on the
  messages view). Both features are read-only/public and fully automatic.

Migration: 0015_add_moderation_explainability.sql
This commit is contained in:
asepharyana committed 2026-08-18 15:11:01 +07:00
1 parent d68f6b653a
commit 1ae19074ee
25 files changed
+1189 -7

No files matched your search

@@ -4,6 +4,7 @@ import { config } from "../../shared/config/config.js";
import { queueMessageAnalysis } from "../ai-moderation/aiAnalyzer.js";
import { processAttachmentUpload } from "../attachment-upload/attachmentUploader.js";
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
import { archiveMessageEmbedded } from "../message-capture/archiveEmbedder.js";
import {
getDisplayContent,
getMessageLocation,
@@ -203,6 +204,7 @@ export async function captureMessage(
type: "text" | "edited" | "deleted",
options: { source?: "live" | "backlog" } = {},
): Promise<void> {
const isBacklog = options.source === "backlog";
const location = getMessageLocation(message);
const messageRecord = buildMessageRecord(message, type);
@@ -211,7 +213,11 @@ export async function captureMessage(
return;
}
const isBacklog = options.source === "backlog";
// Fire-and-forget: make the captured message searchable in the persistent
// archive (public semantic search). Never blocks capture/moderation.
if (!isBacklog && messageRecord.content) {
archiveMessageEmbedded(messageRecord);
}
if (_eventBroadcaster && !isBacklog) {
_eventBroadcaster.messageCreated(messageRecord);