diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f2a37f0..20b0ce1 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -16,4 +16,4 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm typecheck - run: pnpm lint - - run: pnpm test + - run: pnpm coverage diff --git a/DECISIONS.md b/DECISIONS.md index 288a315..91d111f 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -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. - **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. + +## 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. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index ec3bc31..4d40d91 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -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.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). -- [ ] 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`. diff --git a/package.json b/package.json index d0f2df8..804e23e 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,13 @@ "typecheck": "pnpm -r --parallel typecheck", "lint": "eslint . && prettier --check .", "format": "prettier --write .", - "test": "vitest run" + "test": "vitest run", + "coverage": "vitest run --coverage" }, "devDependencies": { "@eslint/js": "^10.0.1", "@types/node": "^24", + "@vitest/coverage-v8": "^5.0.3", "eslint": "^10.12.0", "eslint-config-prettier": "^10.1.8", "globals": "^17.13.0", diff --git a/packages/sim/src/index.ts b/packages/sim/src/index.ts index bfc120c..a771c21 100644 --- a/packages/sim/src/index.ts +++ b/packages/sim/src/index.ts @@ -1,15 +1,63 @@ -export * from "./clock"; -export * from "./rng"; -export * from "./positions"; -export * from "./distance"; -export * from "./travel"; -export * from "./fuel"; -export * from "./eligibility"; -export * from "./contract"; -export * from "./board"; -export * from "./tutorial"; -export * from "./pilot"; -export * from "./dispatch"; -export * from "./flight"; -export * from "./xp"; -export * from "./resolution"; +// Public API of the deterministic simulation. Internal helpers (content +// lookups, contract building) stay unexported. + +export { MS_PER_HOUR, MS_PER_MINUTE, gameToReal, realToGame } from "./clock"; +export type { GameTime, RealTime } from "./clock"; + +export { createRng, deriveSeed } from "./rng"; +export type { Rng, Seed } from "./rng"; + +export { bodyPosition, locationPosition } from "./positions"; +export type { Position } from "./positions"; + +export { routeDistance } from "./distance"; +export { travelTimeGameHours } from "./travel"; +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"; diff --git a/packages/sim/test/smoke.test.ts b/packages/sim/test/smoke.test.ts deleted file mode 100644 index b4afb6f..0000000 --- a/packages/sim/test/smoke.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from "vitest"; - -describe("sim", () => { - it("runs tests", () => { - expect(1 + 1).toBe(2); - }); -}); diff --git a/packages/sim/test/tutorial.test.ts b/packages/sim/test/tutorial.test.ts index 2114b5f..cfc8aef 100644 --- a/packages/sim/test/tutorial.test.ts +++ b/packages/sim/test/tutorial.test.ts @@ -2,11 +2,11 @@ import { content } from "@deadhead/content"; import { describe, expect, it } from "vitest"; import { boardPeriodIndex, - buildContract, generateTutorialBoard, loanerModel, type TutorialBoardInput, } from "../src/index"; +import { buildContract } from "../src/board"; const periodIndex = boardPeriodIndex(Date.parse("2100-01-01T00:00:00Z"), content.config); const atEarth: TutorialBoardInput = { loanerLocationId: "earth", periodIndex, worldSeed: 99 }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4895d26..89ac8c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@types/node': specifier: ^24 version: 24.19.1 + '@vitest/coverage-v8': + specifier: ^5.0.3 + version: 5.0.3(vitest@5.0.3) eslint: specifier: ^10.12.0 version: 10.12.0 @@ -34,7 +37,7 @@ importers: version: 8.71.0(eslint@10.12.0)(typescript@6.0.3) vitest: 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: {} @@ -59,6 +62,27 @@ importers: 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': resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} @@ -322,6 +346,23 @@ packages: resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} 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': resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: @@ -353,6 +394,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@1.0.7: + resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} + astronomy-engine@2.1.19: resolution: {integrity: sha512-8yWKNf7UeNbH458h3sAJ6ZgAjE5jTXp/mNNRFoC20j2SHwZIjAQeEsBB2Q3uCFRaTCCJRv33K2XhkhZQMXoX6w==} @@ -530,6 +574,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -624,6 +671,9 @@ packages: magic-string@1.4.2: resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -728,6 +778,10 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} 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: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -863,6 +917,21 @@ packages: 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': dependencies: '@cacheable/utils': 2.5.0 @@ -1099,6 +1168,24 @@ snapshots: '@typescript-eslint/types': 8.71.0 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))': dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -1125,6 +1212,12 @@ snapshots: 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: {} balanced-match@4.0.4: {} @@ -1293,6 +1386,8 @@ snapshots: isexe@2.0.0: {} + js-tokens@10.0.0: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -1363,6 +1458,12 @@ snapshots: dependencies: '@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: dependencies: brace-expansion: 5.0.12 @@ -1458,6 +1559,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinyrainbow@3.2.0: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -1496,7 +1599,7 @@ snapshots: '@types/node': 24.19.1 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: '@types/chai': 5.2.3 '@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 optionalDependencies: '@types/node': 24.19.1 + '@vitest/coverage-v8': 5.0.3(vitest@5.0.3) transitivePeerDependencies: - msw diff --git a/vitest.config.ts b/vitest.config.ts index 254889d..961f7d2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,5 +5,12 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { 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 }, + }, }, });