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: composite.ts2// Path: packages/scoring/src/composite.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: Occupation-level aggregation: importance-weighted means and highly-exposed task share.910import { scoreTask } from "./task";11import type { OccupationScores, ScoreBand, TaskInput, TaskScores } from "./types";12import { INDEX_VERSION } from "./version";1314/** Substitution score at or above which a task counts as "highly exposed" — METHODOLOGY.md §6. */15export const HIGH_EXPOSURE_THRESHOLD = 70;1617/**18 * Aggregate task scores to an occupation: importance-weighted mean, weights19 * normalized within the occupation — METHODOLOGY.md §6. Pure and deterministic.20 */21export function scoreOccupation(tasks: readonly TaskInput[]): OccupationScores {22 if (tasks.length === 0) {23 throw new RangeError("scoreOccupation: at least one task is required");24 }2526 const taskScores = tasks.map(scoreTask);2728 const provided = tasks29 .map((task) => task.importance)30 .filter((value): value is number => value !== undefined);31 for (const value of provided) {32 if (!Number.isFinite(value) || value < 0) {33 throw new RangeError(`scoreOccupation: importance must be a non-negative number, got ${value}`);34 }35 }36 // Tasks lacking an importance rating receive the occupation-mean importance (§6).37 const fallback =38 provided.length > 0 ? provided.reduce((sum, value) => sum + value, 0) / provided.length : 1;39 const raw = tasks.map((task) => task.importance ?? fallback);40 const rawSum = raw.reduce((sum, value) => sum + value, 0);41 const weights = rawSum > 0 ? raw.map((value) => value / rawSum) : raw.map(() => 1 / raw.length);4243 const aggregate = (pick: (task: TaskScores) => ScoreBand): ScoreBand => {44 let low = 0;45 let score = 0;46 let high = 0;47 for (const [index, task] of taskScores.entries()) {48 const weight = weights[index];49 const band = pick(task);50 low += weight * band.low;51 score += weight * band.score;52 high += weight * band.high;53 }54 return { low, score, high };55 };5657 const highlyExposedTaskShare =58 taskScores.filter((task) => task.substitution.score >= HIGH_EXPOSURE_THRESHOLD).length /59 taskScores.length;6061 return {62 indexVersion: INDEX_VERSION,63 substitution: aggregate((task) => task.substitution),64 exposure: aggregate((task) => task.exposure),65 augmentation: aggregate((task) => task.augmentation),66 highlyExposedTaskShare,67 tasks: taskScores,68 };69}70