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';