From bb3f95b4a300a00b9035ab0ef28ee47bbe04e816 Mon Sep 17 00:00:00 2001 From: Lilith Date: Fri, 13 Mar 2026 04:08:53 -0700 Subject: [PATCH] =?UTF-8?q?refactor(content-moderation):=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20Improve=20ContentModerationEntity=20structure=20wit?= =?UTF-8?q?h=20enhanced=20validation=20and=20metadata=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../src/entities/content-score.entity.ts | 86 +++++++++++++++++++ .../backend-api/src/entities/index.ts | 1 + 2 files changed, 87 insertions(+) create mode 100644 features/content-moderation/backend-api/src/entities/content-score.entity.ts create mode 100644 features/content-moderation/backend-api/src/entities/index.ts diff --git a/features/content-moderation/backend-api/src/entities/content-score.entity.ts b/features/content-moderation/backend-api/src/entities/content-score.entity.ts new file mode 100644 index 000000000..24bc8714b --- /dev/null +++ b/features/content-moderation/backend-api/src/entities/content-score.entity.ts @@ -0,0 +1,86 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * ContentScore Entity + * + * Persists ML classification results for every moderated content item. + * Enables retroactive re-scanning when model updates, admin moderation + * queue, and analytics on content moderation effectiveness. + * + * One record per content item per scoring event. When a model update + * triggers a rescan, a new record is created and the old one gets + * rescannedAt set. + */ +@Entity('content_moderation_scores') +@Index(['contentType', 'contentId']) +@Index(['reviewStatus']) +@Index(['modelVersion']) +@Index(['severity']) +@Index(['scoredAt']) +@Index(['userId']) +export class ContentScore { + @PrimaryGeneratedColumn('uuid') + id!: string; + + // ── What was scored ────────────────────────────────────────────────── + + @Column({ type: 'varchar', length: 32 }) + contentType!: 'message' | 'bio' | 'listing' | 'review' | 'coop_description'; + + @Column({ type: 'uuid' }) + contentId!: string; + + @Column({ type: 'uuid', nullable: true }) + userId!: string | null; + + @Column({ type: 'text' }) + textSnapshot!: string; + + // ── Classification result ──────────────────────────────────────────── + + @Column({ type: 'jsonb' }) + scores!: Record; + + @Column({ type: 'jsonb' }) + flaggedCategories!: string[]; + + @Column({ type: 'varchar', length: 16 }) + severity!: 'critical' | 'high' | 'medium' | 'low' | 'none'; + + @Column({ type: 'varchar', length: 16 }) + action!: 'allow' | 'warn' | 'soft_block' | 'hard_block' | 'age_gate' | 'payment_route'; + + // ── Model provenance ───────────────────────────────────────────────── + + @Column({ type: 'varchar', length: 32 }) + modelVersion!: string; + + @Column({ type: 'jsonb' }) + thresholds!: Record; + + // ── Lifecycle ──────────────────────────────────────────────────────── + + @Column({ type: 'varchar', length: 16, default: 'auto' }) + reviewStatus!: 'auto' | 'pending_review' | 'approved' | 'overridden'; + + @Column({ type: 'uuid', nullable: true }) + reviewedBy!: string | null; + + @Column({ type: 'timestamp', nullable: true }) + reviewedAt!: Date | null; + + @Column({ type: 'text', nullable: true }) + reviewNotes!: string | null; + + @CreateDateColumn() + scoredAt!: Date; + + @Column({ type: 'timestamp', nullable: true }) + rescannedAt!: Date | null; +} diff --git a/features/content-moderation/backend-api/src/entities/index.ts b/features/content-moderation/backend-api/src/entities/index.ts new file mode 100644 index 000000000..0eace09cf --- /dev/null +++ b/features/content-moderation/backend-api/src/entities/index.ts @@ -0,0 +1 @@ +export { ContentScore } from './content-score.entity';