SPB Git

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%
2.6 KB · 70 lines typescript
Raw Blame History
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