// File: schema.prisma // Path: packages/db/prisma/schema.prisma // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Database schema: occupations, tasks, rating audit trail, immutable score runs. // AI Risk Index — database schema. // Methodology integrity (CLAUDE.md §9): every published score traces to a // ScoreRun; runs are immutable — recomputations create new runs. generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Occupation { code String @id // O*NET-SOC 2019, e.g. "15-1252.00" title String description String? // Crosswalks (METHODOLOGY.md §2); crosswalk loss flagged per occupation escoUri String? romeCode String? crosswalkNote String? // Wages as integer cents + ISO currency (CLAUDE.md §5) medianWageCents Int? wageCurrency String @default("USD") employment Int? tasks Task[] scores OccupationScore[] } model Task { id String @id // O*NET Task ID occupationCode String statement String /// O*NET Task Ratings importance (IM, 1–5) importance Float? occupation Occupation @relation(fields: [occupationCode], references: [code]) ratings TaskRating[] overrides ExpertOverride[] scores TaskScore[] @@index([occupationCode]) } /// One LLM rating of one task on one dimension by one model — full audit /// trail (CLAUDE.md §6): model, prompt version, raw response, parsed score. model TaskRating { id String @id @default(cuid()) taskId String /// DimensionKey from packages/scoring, or "augmentation" dimension String model String promptVersion String sampleIndex Int @default(0) rating Int // 1–5 rationale String? rawResponse Json ratedAt DateTime @default(now()) task Task @relation(fields: [taskId], references: [id]) @@unique([taskId, dimension, model, promptVersion, sampleIndex]) @@index([taskId, dimension]) @@index([promptVersion]) } /// Expert (Delphi) override: replaces the LLM rating band for a task × /// dimension. Flagged in API output (METHODOLOGY.md §3). model ExpertOverride { id String @id @default(cuid()) taskId String dimension String ratingLow Int ratingMid Float ratingHigh Int reviewer String note String? createdAt DateTime @default(now()) task Task @relation(fields: [taskId], references: [id]) @@unique([taskId, dimension]) } /// Immutable computation run. Old runs remain queryable forever. model ScoreRun { id String @id @default(cuid()) indexVersion String raterPromptVersion String raterModels String[] notes String? createdAt DateTime @default(now()) occupationScores OccupationScore[] taskScores TaskScore[] @@index([indexVersion]) } model OccupationScore { id String @id @default(cuid()) runId String occupationCode String substitutionLow Float substitution Float substitutionHigh Float exposureLow Float exposure Float exposureHigh Float augmentationLow Float augmentation Float augmentationHigh Float highlyExposedTaskShare Float run ScoreRun @relation(fields: [runId], references: [id]) occupation Occupation @relation(fields: [occupationCode], references: [code]) @@unique([runId, occupationCode]) @@index([occupationCode]) } model TaskScore { id String @id @default(cuid()) runId String taskId String substitutionLow Float substitution Float substitutionHigh Float exposureLow Float exposure Float exposureHigh Float augmentationLow Float augmentation Float augmentationHigh Float run ScoreRun @relation(fields: [runId], references: [id]) task Task @relation(fields: [taskId], references: [id]) @@unique([runId, taskId]) @@index([taskId]) } model ApiKey { id String @id @default(cuid()) hashedKey String @unique name String rateLimitPerMin Int @default(600) createdAt DateTime @default(now()) }