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.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