spb/airiskindex Public
The most methodologically rigorous, fully transparent AI job-exposure index.
TypeScript 88%
Python 6.1%
SQL 2.7%
CSS 1.2%
JavaScript 0.9%
Shell 0.8%
1// File: recompute.ts2// Path: apps/worker/src/scripts/recompute.ts3// Project: AI Risk Index — airiskindex.io4// Author: Simon-Pierre Boucher5// Contact: contact@spboucher.ai6// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.7//8// Description: Full index recomputation: panel bands + overrides -> new immutable score run.910import { prisma } from "@airiskindex/db";11import {12 INDEX_VERSION,13 scoreOccupation,14 type RatingBand,15 type TaskInput,16 type TaskRatings,17} from "@airiskindex/scoring";18import { RATED_DIMENSIONS } from "../raters/schema";19import { RATER_MODELS, RATER_PROMPT_VERSION } from "../config";2021// Full index recomputation (`pnpm score:recompute`): builds rating bands from22// the multi-model panel (min/mean/max across models — METHODOLOGY.md §3),23// applies expert overrides, scores every occupation, and writes ONE new24// immutable ScoreRun. Historical runs are never mutated (CLAUDE.md §9).2526function bandFromPanel(values: number[]): RatingBand {27 const low = Math.min(...values);28 const high = Math.max(...values);29 const mid = values.reduce((sum, value) => sum + value, 0) / values.length;30 return { low, mid, high };31}3233async function main(): Promise<void> {34 // Load occupations lazily, one at a time, selecting only the rating fields35 // the computation needs — a full include would materialize every raw API36 // response (~1 GB of JSON) in one query and overflow the engine bridge.37 const occupationCodes = await prisma.occupation.findMany({ select: { code: true } });3839 const run = await prisma.scoreRun.create({40 data: {41 indexVersion: INDEX_VERSION,42 raterPromptVersion: RATER_PROMPT_VERSION,43 raterModels: [...RATER_MODELS],44 },45 });4647 let scoredOccupations = 0;48 let skippedTasks = 0;4950 for (const { code } of occupationCodes) {51 const occupation = await prisma.occupation.findUniqueOrThrow({52 where: { code },53 select: {54 code: true,55 tasks: {56 select: {57 id: true,58 importance: true,59 ratings: {60 where: { promptVersion: RATER_PROMPT_VERSION },61 select: { dimension: true, rating: true },62 },63 overrides: {64 select: { dimension: true, ratingLow: true, ratingMid: true, ratingHigh: true },65 },66 },67 },68 },69 });70 const inputs: TaskInput[] = [];7172 for (const task of occupation.tasks) {73 const bands: Partial<Record<(typeof RATED_DIMENSIONS)[number], RatingBand>> = {};74 let complete = true;7576 for (const dimension of RATED_DIMENSIONS) {77 const override = task.overrides.find((entry) => entry.dimension === dimension);78 if (override) {79 bands[dimension] = {80 low: override.ratingLow,81 mid: override.ratingMid,82 high: override.ratingHigh,83 };84 continue;85 }86 const values = task.ratings87 .filter((entry) => entry.dimension === dimension)88 .map((entry) => entry.rating);89 if (values.length === 0) {90 complete = false;91 break;92 }93 bands[dimension] = bandFromPanel(values);94 }9596 if (!complete) {97 skippedTasks += 1;98 continue;99 }100 inputs.push({101 taskId: task.id,102 importance: task.importance ?? undefined,103 ratings: bands as TaskRatings,104 });105 }106107 if (inputs.length === 0) continue;108109 const scores = scoreOccupation(inputs);110 await prisma.$transaction([111 prisma.occupationScore.create({112 data: {113 runId: run.id,114 occupationCode: occupation.code,115 substitutionLow: scores.substitution.low,116 substitution: scores.substitution.score,117 substitutionHigh: scores.substitution.high,118 exposureLow: scores.exposure.low,119 exposure: scores.exposure.score,120 exposureHigh: scores.exposure.high,121 augmentationLow: scores.augmentation.low,122 augmentation: scores.augmentation.score,123 augmentationHigh: scores.augmentation.high,124 highlyExposedTaskShare: scores.highlyExposedTaskShare,125 },126 }),127 ...scores.tasks.map((task) =>128 prisma.taskScore.create({129 data: {130 runId: run.id,131 taskId: task.taskId,132 substitutionLow: task.substitution.low,133 substitution: task.substitution.score,134 substitutionHigh: task.substitution.high,135 exposureLow: task.exposure.low,136 exposure: task.exposure.score,137 exposureHigh: task.exposure.high,138 augmentationLow: task.augmentation.low,139 augmentation: task.augmentation.score,140 augmentationHigh: task.augmentation.high,141 },142 }),143 ),144 ]);145 scoredOccupations += 1;146 }147148 console.log(149 `Run ${run.id} (index ${INDEX_VERSION}, prompt ${RATER_PROMPT_VERSION}): scored ${scoredOccupations}/${occupationCodes.length} occupations; skipped ${skippedTasks} unrated tasks.`,150 );151}152153main()154 .catch((error) => {155 console.error(error);156 process.exitCode = 1;157 })158 .finally(() => prisma.$disconnect());159