Step 3.18: define sim public API and add coverage
CI / check (push) Successful in 38s Details

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-02 18:51:49 -04:00
parent 50b038047f
commit f33a9155cc
9 changed files with 188 additions and 28 deletions

View File

@ -16,4 +16,4 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm typecheck - run: pnpm typecheck
- run: pnpm lint - run: pnpm lint
- run: pnpm test - run: pnpm coverage

View File

@ -176,3 +176,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **Tutorial runs grant XP to the avatar** like any delivery; the plan does not exclude it. They credit the stored tutorial payout with no pilot pay. - **Tutorial runs grant XP to the avatar** like any delivery; the plan does not exclude it. They credit the stored tutorial payout with no pilot pay.
- **Repositions resolve to 0 credits and 0 XP** and never discover anything, since they only fly to bays. - **Repositions resolve to 0 credits and 0 XP** and never discover anything, since they only fly to bays.
- **The caller passes `pilotIsAvatar` and the current discovered set**, so the function stays pure. "Exactly once" is the server's job: it must store the resolution and the acknowledged-at state transactionally. - **The caller passes `pilotIsAvatar` and the current discovered set**, so the function stays pure. "Exactly once" is the server's job: it must store the resolution and the acknowledged-at state transactionally.
## Step 3.18
- **Explicit public API.** `packages/sim/src/index.ts` lists every export. Internal helpers (`findLocation`, `findShipModel`, `standardCargo`, `buildContract`, `skillLevel`) are not exported; tests that need one import it from its module.
- **Coverage** uses `@vitest/coverage-v8`, configured in the root `vitest.config.ts` to measure only `packages/sim/src` with a 90% line threshold. `pnpm coverage` runs the whole suite with coverage, and CI runs it in place of `pnpm test`, so a coverage drop fails CI.
- **The Step 1.3 smoke test is removed** now that the sim package has real tests.

View File

@ -36,4 +36,4 @@
- [x] **Step 3.15 — In-flight position and progress.** `src/flight.ts`: `dispatchProgress` (clamped 0–1) and `dispatchPosition` (straight line from origin at departure to destination at arrival). - [x] **Step 3.15 — In-flight position and progress.** `src/flight.ts`: `dispatchProgress` (clamped 0–1) and `dispatchPosition` (straight line from origin at departure to destination at arrival).
- [x] **Step 3.16 — XP and leveling.** `src/xp.ts`: `deliveryXp`, `pilotLevel`, `skillPointsAvailable`. - [x] **Step 3.16 — XP and leveling.** `src/xp.ts`: `deliveryXp`, `pilotLevel`, `skillPointsAvailable`.
- [x] **Step 3.17 — Dispatch resolution.** `src/resolution.ts`: `resolveDispatch` returns `in-flight` or a `Resolution` (credits after pilot pay, XP, new ship location, discovery). - [x] **Step 3.17 — Dispatch resolution.** `src/resolution.ts`: `resolveDispatch` returns `in-flight` or a `Resolution` (credits after pilot pay, XP, new ship location, discovery).
- [ ] Step 3.18 — Sim package public API and coverage - [x] **Step 3.18 — Public API and coverage.** `src/index.ts` exports an explicit list (content lookups and contract building stay internal). `pnpm coverage` (V8, `packages/sim/src`, 90% line threshold) reports 100% lines; CI now runs `pnpm coverage` instead of `pnpm test`.

View File

@ -10,11 +10,13 @@
"typecheck": "pnpm -r --parallel typecheck", "typecheck": "pnpm -r --parallel typecheck",
"lint": "eslint . && prettier --check .", "lint": "eslint . && prettier --check .",
"format": "prettier --write .", "format": "prettier --write .",
"test": "vitest run" "test": "vitest run",
"coverage": "vitest run --coverage"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/node": "^24", "@types/node": "^24",
"@vitest/coverage-v8": "^5.0.3",
"eslint": "^10.12.0", "eslint": "^10.12.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"globals": "^17.13.0", "globals": "^17.13.0",

View File

@ -1,15 +1,63 @@
export * from "./clock"; // Public API of the deterministic simulation. Internal helpers (content
export * from "./rng"; // lookups, contract building) stay unexported.
export * from "./positions";
export * from "./distance"; export { MS_PER_HOUR, MS_PER_MINUTE, gameToReal, realToGame } from "./clock";
export * from "./travel"; export type { GameTime, RealTime } from "./clock";
export * from "./fuel";
export * from "./eligibility"; export { createRng, deriveSeed } from "./rng";
export * from "./contract"; export type { Rng, Seed } from "./rng";
export * from "./board";
export * from "./tutorial"; export { bodyPosition, locationPosition } from "./positions";
export * from "./pilot"; export type { Position } from "./positions";
export * from "./dispatch";
export * from "./flight"; export { routeDistance } from "./distance";
export * from "./xp"; export { travelTimeGameHours } from "./travel";
export * from "./resolution"; export { fuelCost } from "./fuel";
export { canLandAt, checkRoute } from "./eligibility";
export type { RouteCheck } from "./eligibility";
export { contractPayout } from "./contract";
export type { Contract, PayoutInput } from "./contract";
export {
boardPeriodIndex,
boardPeriodMs,
generateContractBoard,
requiredLongHaulLevel,
} from "./board";
export type { BoardInput, SimContent } from "./board";
export { generateTutorialBoard, loanerModel } from "./tutorial";
export type { TutorialBoardInput, TutorialLocationId } from "./tutorial";
export { LONG_HAUL_SKILL_ID, checkPilotEligibility } from "./pilot";
export type { PilotCheck, SkillLevels } from "./pilot";
export {
createDispatchRecord,
createRepositionRecord,
tripFuelCost,
validateDispatch,
validateReposition,
} from "./dispatch";
export type {
DispatchCheck,
DispatchFailure,
DispatchProposal,
DispatchRecord,
PilotState,
RecordInput,
RepositionCheck,
RepositionFailure,
RepositionProposal,
RepositionRecordInput,
ShipState,
} from "./dispatch";
export { dispatchPosition, dispatchProgress } from "./flight";
export { deliveryXp, pilotLevel, skillPointsAvailable } from "./xp";
export { resolveDispatch } from "./resolution";
export type { Resolution, ResolveInput, ResolveResult } from "./resolution";

View File

@ -1,7 +0,0 @@
import { describe, expect, it } from "vitest";
describe("sim", () => {
it("runs tests", () => {
expect(1 + 1).toBe(2);
});
});

View File

@ -2,11 +2,11 @@ import { content } from "@deadhead/content";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
boardPeriodIndex, boardPeriodIndex,
buildContract,
generateTutorialBoard, generateTutorialBoard,
loanerModel, loanerModel,
type TutorialBoardInput, type TutorialBoardInput,
} from "../src/index"; } from "../src/index";
import { buildContract } from "../src/board";
const periodIndex = boardPeriodIndex(Date.parse("2100-01-01T00:00:00Z"), content.config); const periodIndex = boardPeriodIndex(Date.parse("2100-01-01T00:00:00Z"), content.config);
const atEarth: TutorialBoardInput = { loanerLocationId: "earth", periodIndex, worldSeed: 99 }; const atEarth: TutorialBoardInput = { loanerLocationId: "earth", periodIndex, worldSeed: 99 };

View File

@ -14,6 +14,9 @@ importers:
'@types/node': '@types/node':
specifier: ^24 specifier: ^24
version: 24.19.1 version: 24.19.1
'@vitest/coverage-v8':
specifier: ^5.0.3
version: 5.0.3(vitest@5.0.3)
eslint: eslint:
specifier: ^10.12.0 specifier: ^10.12.0
version: 10.12.0 version: 10.12.0
@ -34,7 +37,7 @@ importers:
version: 8.71.0(eslint@10.12.0)(typescript@6.0.3) version: 8.71.0(eslint@10.12.0)(typescript@6.0.3)
vitest: vitest:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3(@types/node@24.19.1)(vite@8.3.2(@types/node@24.19.1)) version: 5.0.3(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1))
apps/server: {} apps/server: {}
@ -59,6 +62,27 @@ importers:
packages: packages:
'@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.9':
resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/types@7.29.8':
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@cacheable/memory@2.2.0': '@cacheable/memory@2.2.0':
resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==}
@ -322,6 +346,23 @@ packages:
resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@5.0.3':
resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==}
peerDependencies:
'@vitest/browser': 5.0.3
vitest: 5.0.3
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/istanbul-lib-coverage@1.0.2':
resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==}
engines: {node: '>=22'}
'@vitest/istanbul-lib-report@1.0.2':
resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==}
engines: {node: '>=22'}
'@vitest/mocker@5.0.3': '@vitest/mocker@5.0.3':
resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
peerDependencies: peerDependencies:
@ -353,6 +394,9 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
ast-v8-to-istanbul@1.0.7:
resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==}
astronomy-engine@2.1.19: astronomy-engine@2.1.19:
resolution: {integrity: sha512-8yWKNf7UeNbH458h3sAJ6ZgAjE5jTXp/mNNRFoC20j2SHwZIjAQeEsBB2Q3uCFRaTCCJRv33K2XhkhZQMXoX6w==} resolution: {integrity: sha512-8yWKNf7UeNbH458h3sAJ6ZgAjE5jTXp/mNNRFoC20j2SHwZIjAQeEsBB2Q3uCFRaTCCJRv33K2XhkhZQMXoX6w==}
@ -530,6 +574,9 @@ packages:
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
json-schema-traverse@0.4.1: json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@ -624,6 +671,9 @@ packages:
magic-string@1.4.2: magic-string@1.4.2:
resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==} resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==}
magicast@0.5.5:
resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==}
minimatch@10.2.6: minimatch@10.2.6:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@ -728,6 +778,10 @@ packages:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinyrainbow@3.2.0:
resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==}
engines: {node: '>=14.0.0'}
ts-api-utils@2.5.0: ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'} engines: {node: '>=18.12'}
@ -863,6 +917,21 @@ packages:
snapshots: snapshots:
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/parser@7.29.9':
dependencies:
'@babel/types': 7.29.8
'@babel/types@7.29.8':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@bcoe/v8-coverage@1.0.2': {}
'@cacheable/memory@2.2.0': '@cacheable/memory@2.2.0':
dependencies: dependencies:
'@cacheable/utils': 2.5.0 '@cacheable/utils': 2.5.0
@ -1099,6 +1168,24 @@ snapshots:
'@typescript-eslint/types': 8.71.0 '@typescript-eslint/types': 8.71.0
eslint-visitor-keys: 5.0.1 eslint-visitor-keys: 5.0.1
'@vitest/coverage-v8@5.0.3(vitest@5.0.3)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/istanbul-lib-coverage': 1.0.2
'@vitest/istanbul-lib-report': 1.0.2
ast-v8-to-istanbul: 1.0.7
magicast: 0.5.5
obug: 2.2.1
std-env: 4.3.0
tinyrainbow: 3.2.0
vitest: 5.0.3(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1))
'@vitest/istanbul-lib-coverage@1.0.2': {}
'@vitest/istanbul-lib-report@1.0.2':
dependencies:
'@vitest/istanbul-lib-coverage': 1.0.2
'@vitest/mocker@5.0.3(vite@8.3.2(@types/node@24.19.1))': '@vitest/mocker@5.0.3(vite@8.3.2(@types/node@24.19.1))':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
@ -1125,6 +1212,12 @@ snapshots:
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
ast-v8-to-istanbul@1.0.7:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 10.0.0
astronomy-engine@2.1.19: {} astronomy-engine@2.1.19: {}
balanced-match@4.0.4: {} balanced-match@4.0.4: {}
@ -1293,6 +1386,8 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
js-tokens@10.0.0: {}
json-schema-traverse@0.4.1: {} json-schema-traverse@0.4.1: {}
json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {}
@ -1363,6 +1458,12 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/sourcemap-codec': 1.6.0
magicast@0.5.5:
dependencies:
'@babel/parser': 7.29.9
'@babel/types': 7.29.8
source-map-js: 1.2.2
minimatch@10.2.6: minimatch@10.2.6:
dependencies: dependencies:
brace-expansion: 5.0.12 brace-expansion: 5.0.12
@ -1458,6 +1559,8 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.7) fdir: 6.5.0(picomatch@4.0.7)
picomatch: 4.0.7 picomatch: 4.0.7
tinyrainbow@3.2.0: {}
ts-api-utils@2.5.0(typescript@6.0.3): ts-api-utils@2.5.0(typescript@6.0.3):
dependencies: dependencies:
typescript: 6.0.3 typescript: 6.0.3
@ -1496,7 +1599,7 @@ snapshots:
'@types/node': 24.19.1 '@types/node': 24.19.1
fsevents: 2.3.3 fsevents: 2.3.3
vitest@5.0.3(@types/node@24.19.1)(vite@8.3.2(@types/node@24.19.1)): vitest@5.0.3(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/mocker': 5.0.3(vite@8.3.2(@types/node@24.19.1)) '@vitest/mocker': 5.0.3(vite@8.3.2(@types/node@24.19.1))
@ -1514,6 +1617,7 @@ snapshots:
why-is-node-running: 3.2.1 why-is-node-running: 3.2.1
optionalDependencies: optionalDependencies:
'@types/node': 24.19.1 '@types/node': 24.19.1
'@vitest/coverage-v8': 5.0.3(vitest@5.0.3)
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw

View File

@ -5,5 +5,12 @@ import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
test: { test: {
projects: ["apps/*", "packages/*"], projects: ["apps/*", "packages/*"],
// `pnpm coverage` measures the simulation core (plan Step 3.18).
coverage: {
provider: "v8",
include: ["packages/sim/src/**/*.ts"],
reporter: ["text", "html"],
thresholds: { lines: 90 },
},
}, },
}); });