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.1 KB · 81 lines typescript
Raw Blame History
1//  File:    composite.test.ts2//  Path:    packages/scoring/src/composite.test.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: Snapshot tests against the published methodology worked example.910import { readFileSync } from "node:fs";11import { describe, expect, it } from "vitest";12import { HIGH_EXPOSURE_THRESHOLD, scoreOccupation } from "./index";1314// Published worked example — METHODOLOGY.md §5-6. Changing it requires an INDEX_VERSION bump.15const example = JSON.parse(16  readFileSync(new URL("../../../docs/methodology/examples/example-analyst.json", import.meta.url), "utf8"),17);1819const BAND_KEYS = ["substitution", "exposure", "augmentation"] as const;2021describe("published methodology example (example-analyst.json)", () => {22  const result = scoreOccupation(example.tasks);2324  it("matches the expected task scores", () => {25    expect(result.tasks).toHaveLength(example.expected.taskScores.length);26    for (const [index, expected] of example.expected.taskScores.entries()) {27      const actual = result.tasks[index];28      expect(actual.taskId).toBe(expected.taskId);29      for (const key of BAND_KEYS) {30        expect(actual[key].low).toBeCloseTo(expected[key].low, 3);31        expect(actual[key].score).toBeCloseTo(expected[key].score, 3);32        expect(actual[key].high).toBeCloseTo(expected[key].high, 3);33      }34    }35  });3637  it("matches the expected occupation scores", () => {38    for (const key of BAND_KEYS) {39      expect(result[key].low).toBeCloseTo(example.expected.occupation[key].low, 3);40      expect(result[key].score).toBeCloseTo(example.expected.occupation[key].score, 3);41      expect(result[key].high).toBeCloseTo(example.expected.occupation[key].high, 3);42    }43  });4445  it("reports the highly exposed task share", () => {46    expect(HIGH_EXPOSURE_THRESHOLD).toBe(70);47    expect(result.highlyExposedTaskShare).toBeCloseTo(48      example.expected.occupation.highlyExposedTaskShare,49      3,50    );51  });52});5354describe("scoreOccupation input handling", () => {55  it("rejects an empty task list", () => {56    expect(() => scoreOccupation([])).toThrow(RangeError);57  });5859  it("fills missing importance with the occupation mean", () => {60    const [t1, t2, t3] = example.tasks;61    const withMissing = [t1, { ...t2, importance: undefined }, t3];62    const result = scoreOccupation(withMissing);63    // t2 gets mean(4, 1) = 2.5 → weights 4/7.5, 2.5/7.5, 1/7.564    const expected =65      (4 / 7.5) * 71.25 + (2.5 / 7.5) * 27.5 + (1 / 7.5) * 86.25;66    expect(result.substitution.score).toBeCloseTo(expected, 3);67  });6869  it("rejects out-of-range ratings", () => {70    const bad = JSON.parse(JSON.stringify(example.tasks[0]));71    bad.ratings.automatability.mid = 6;72    expect(() => scoreOccupation([bad])).toThrow(RangeError);73  });7475  it("rejects inverted bands (low > high)", () => {76    const bad = JSON.parse(JSON.stringify(example.tasks[0]));77    bad.ratings.feasibility = { low: 4, mid: 3, high: 2 };78    expect(() => scoreOccupation([bad])).toThrow(RangeError);79  });80});81