// File: recompute.ts // Path: apps/worker/src/scripts/recompute.ts // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Full index recomputation: panel bands + overrides -> new immutable score run. import { prisma } from "@airiskindex/db"; import { INDEX_VERSION, scoreOccupation, type RatingBand, type TaskInput, type TaskRatings, } from "@airiskindex/scoring"; import { RATED_DIMENSIONS } from "../raters/schema"; import { RATER_MODELS, RATER_PROMPT_VERSION } from "../config"; // Full index recomputation (`pnpm score:recompute`): builds rating bands from // the multi-model panel (min/mean/max across models — METHODOLOGY.md §3), // applies expert overrides, scores every occupation, and writes ONE new // immutable ScoreRun. Historical runs are never mutated (CLAUDE.md §9). function bandFromPanel(values: number[]): RatingBand { const low = Math.min(...values); const high = Math.max(...values); const mid = values.reduce((sum, value) => sum + value, 0) / values.length; return { low, mid, high }; } async function main(): Promise { // Load occupations lazily, one at a time, selecting only the rating fields // the computation needs — a full include would materialize every raw API // response (~1 GB of JSON) in one query and overflow the engine bridge. const occupationCodes = await prisma.occupation.findMany({ select: { code: true } }); const run = await prisma.scoreRun.create({ data: { indexVersion: INDEX_VERSION, raterPromptVersion: RATER_PROMPT_VERSION, raterModels: [...RATER_MODELS], }, }); let scoredOccupations = 0; let skippedTasks = 0; for (const { code } of occupationCodes) { const occupation = await prisma.occupation.findUniqueOrThrow({ where: { code }, select: { code: true, tasks: { select: { id: true, importance: true, ratings: { where: { promptVersion: RATER_PROMPT_VERSION }, select: { dimension: true, rating: true }, }, overrides: { select: { dimension: true, ratingLow: true, ratingMid: true, ratingHigh: true }, }, }, }, }, }); const inputs: TaskInput[] = []; for (const task of occupation.tasks) { const bands: Partial> = {}; let complete = true; for (const dimension of RATED_DIMENSIONS) { const override = task.overrides.find((entry) => entry.dimension === dimension); if (override) { bands[dimension] = { low: override.ratingLow, mid: override.ratingMid, high: override.ratingHigh, }; continue; } const values = task.ratings .filter((entry) => entry.dimension === dimension) .map((entry) => entry.rating); if (values.length === 0) { complete = false; break; } bands[dimension] = bandFromPanel(values); } if (!complete) { skippedTasks += 1; continue; } inputs.push({ taskId: task.id, importance: task.importance ?? undefined, ratings: bands as TaskRatings, }); } if (inputs.length === 0) continue; const scores = scoreOccupation(inputs); await prisma.$transaction([ prisma.occupationScore.create({ data: { runId: run.id, occupationCode: occupation.code, substitutionLow: scores.substitution.low, substitution: scores.substitution.score, substitutionHigh: scores.substitution.high, exposureLow: scores.exposure.low, exposure: scores.exposure.score, exposureHigh: scores.exposure.high, augmentationLow: scores.augmentation.low, augmentation: scores.augmentation.score, augmentationHigh: scores.augmentation.high, highlyExposedTaskShare: scores.highlyExposedTaskShare, }, }), ...scores.tasks.map((task) => prisma.taskScore.create({ data: { runId: run.id, taskId: task.taskId, substitutionLow: task.substitution.low, substitution: task.substitution.score, substitutionHigh: task.substitution.high, exposureLow: task.exposure.low, exposure: task.exposure.score, exposureHigh: task.exposure.high, augmentationLow: task.augmentation.low, augmentation: task.augmentation.score, augmentationHigh: task.augmentation.high, }, }), ), ]); scoredOccupations += 1; } console.log( `Run ${run.id} (index ${INDEX_VERSION}, prompt ${RATER_PROMPT_VERSION}): scored ${scoredOccupations}/${occupationCodes.length} occupations; skipped ${skippedTasks} unrated tasks.`, ); } main() .catch((error) => { console.error(error); process.exitCode = 1; }) .finally(() => prisma.$disconnect());