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%
3.7 KB · 109 lines typescript
Raw Blame History
1//  File:    task.ts2//  Path:    packages/scoring/src/task.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: Task-level scoring: rating normalization, orientation, weighted score bands.910import type { RatingBand, ScoreBand, TaskInput, TaskScores } from "./types";11import {12  DIMENSIONS,13  type DimensionKey,14  EXPOSURE_DIMENSIONS,15  INVERTED_DIMENSIONS,16  WEIGHTS,17} from "./weights";1819function assertRating(value: number, context: string): void {20  if (!Number.isFinite(value) || value < 1 || value > 5) {21    throw new RangeError(`${context}: rating must be within [1, 5], got ${value}`);22  }23}2425function assertBand(band: RatingBand, context: string): void {26  assertRating(band.low, `${context}.low`);27  assertRating(band.mid, `${context}.mid`);28  assertRating(band.high, `${context}.high`);29  if (band.low > band.mid || band.mid > band.high) {30    throw new RangeError(`${context}: band must satisfy low ≤ mid ≤ high`);31  }32}3334interface PressureBand {35  low: number;36  mid: number;37  high: number;38}3940/** METHODOLOGY.md §5: rating (1–5) → substitution pressure in [0, 1]. */41export function pressure(dimension: DimensionKey, rating: number): number {42  const p = (rating - 1) / 4;43  return INVERTED_DIMENSIONS.has(dimension) ? 1 - p : p;44}4546/**47 * Pressure bounds for a rating band. For inverted dimensions the rating's48 * HIGH bound minimizes pressure, so the bounds swap — this is what keeps49 * low ≤ score ≤ high true for every dimension orientation.50 */51function pressureBand(dimension: DimensionKey, band: RatingBand): PressureBand {52  const inverted = INVERTED_DIMENSIONS.has(dimension);53  return {54    low: pressure(dimension, inverted ? band.high : band.low),55    mid: pressure(dimension, band.mid),56    high: pressure(dimension, inverted ? band.low : band.high),57  };58}5960function weightedScore(61  pressures: ReadonlyMap<DimensionKey, PressureBand>,62  dimensions: readonly DimensionKey[],63  bound: keyof PressureBand,64): number {65  let total = 0;66  let weightSum = 0;67  for (const dimension of dimensions) {68    const band = pressures.get(dimension);69    if (!band) throw new RangeError(`missing pressure for dimension "${dimension}"`);70    total += WEIGHTS[dimension] * band[bound];71    weightSum += WEIGHTS[dimension];72  }73  return (100 * total) / weightSum;74}7576function toScoreBand(compute: (bound: keyof PressureBand) => number): ScoreBand {77  return { low: compute("low"), score: compute("mid"), high: compute("high") };78}7980/** Score a single task — formulas in METHODOLOGY.md §5. Pure and deterministic. */81export function scoreTask(input: TaskInput): TaskScores {82  const pressures = new Map<DimensionKey, PressureBand>();83  for (const dimension of DIMENSIONS) {84    const band = input.ratings[dimension];85    if (!band) {86      throw new RangeError(`task ${input.taskId}: missing rating for dimension "${dimension}"`);87    }88    assertBand(band, `task ${input.taskId}.${dimension}`);89    pressures.set(dimension, pressureBand(dimension, band));90  }9192  const augmentation = input.ratings.augmentation;93  if (!augmentation) {94    throw new RangeError(`task ${input.taskId}: missing augmentation rating`);95  }96  assertBand(augmentation, `task ${input.taskId}.augmentation`);9798  return {99    taskId: input.taskId,100    substitution: toScoreBand((bound) => weightedScore(pressures, DIMENSIONS, bound)),101    exposure: toScoreBand((bound) => weightedScore(pressures, EXPOSURE_DIMENSIONS, bound)),102    augmentation: {103      low: (100 * (augmentation.low - 1)) / 4,104      score: (100 * (augmentation.mid - 1)) / 4,105      high: (100 * (augmentation.high - 1)) / 4,106    },107  };108}109