chore: modular game/ + test/ layout, concurrent CI, docs consolidation#93
Merged
Conversation
…docs consolidation Group the ~44 loose files under src/app/game/ into responsibility subfolders (scene/ world/ agents/ population/ events/ actions/ execution/ economy/ skills/ objects/ history/), keeping GameManager/City/Clock at the root and data//save/ unchanged. All alias imports remapped to game/<group>/<File>. Mirror the grouping in test/ (one folder per module + util/ for pure utils) and convert every test import from ../src/... relative paths to path aliases (per CLAUDE.md 5.5), so tests are location-independent. Turn each test folder into a Jest project. CI (.github/workflows/ci.yml) now runs separate concurrent checks: a `changes` path-filter, one `test (<module>)` job per affected module (shared/foundational changes fan out to all), typecheck, build, and a full-suite `coverage` job enforcing PER-MODULE thresholds via scripts/coverage-gate.mjs. A stable `ci-success` job aggregates them (make it the required status check). world/agents/save carry documented lower coverage floors (irreducibly-browser Phaser/localStorage code — task 008 territory). Docs: move generated docs to docs/generated/; fold docs/simulation-flows.md into CLAUDE.md 4.14 and delete it; update CLAUDE.md tree/scripts/references and README layout + CI sections. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ds reports) Rework the coverage design per review feedback: each `test (<module>)` job now runs with --coverage and uploads its own per-module report (the module measured by its OWN tests). The `coverage` job no longer runs tests — it downloads every module's report and fails if any module's statement coverage is below a single COVERAGE_THRESHOLD (jest.config.js, currently 72%). Most modules are far below 72% in isolation today (the suite is integration-heavy), which is intentional: it's a forcing function to grow each module's unit tests. The `coverage` check is ADVISORY for now (not in ci-success's needs, so it doesn't block merges); flip it to blocking by adding `coverage` to ci-success once modules climb. Drops the earlier per-module lower-floor exceptions (world/agents/save) in favor of one flat threshold. Docs updated (CLAUDE.md 5.3/2, README CI). gitignore coverage-artifacts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…isory forcing function Add a `lint` CI job plus `npm run lint` (lint:md = markdownlint-cli2, lint:js = eslint . --max-warnings 0). ESLint (typescript-eslint + import + react/react-hooks, eslint.config.mjs) and markdownlint (.markdownlint-cli2.jsonc) reproduce the VS Code Problems panel in CI. Both linters run even if one fails, so all problems surface. Committing the configs + devDependencies is what makes formerly local-only problems consistent: after `npm install`, the VS Code ESLint/markdownlint extensions use the SAME repo rules, so local and CI agree. .vscode/extensions.json recommends both extensions. Red today by design (~819 ESLint problems + markdown findings) — a forcing function to fix the backlog. The `lint` job is ADVISORY (not in ci-success), so it doesn't block merges; add it to ci-success's needs to make it blocking once cleared. Docs: CLAUDE.md 2/5.3, README CI + tech-stack updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Resolution=bundler) Two VS Code "Problems" that didn't reproduce in CI: 1. scripts using node: builtins reported "Cannot find name 'node:fs'..." — @types/node was only present transitively, so the editor (and any differently-resolved install) flagged it while `tsc` passed. Declare @types/node@^20 (matches CI's Node 20) as a devDependency, so `npm install` resolves it everywhere and the CI `typecheck` job is the reproduction mechanism for this class going forward. 2. tsconfig `baseUrl` and `moduleResolution: node` are deprecated (error in the editor's newer TS; the project's pinned 5.4.5 doesn't emit them). Fix at the source: drop baseUrl (paths resolve relative to tsconfig without it) and switch moduleResolution to "bundler" (the modern replacement for "node"/node10, correct for a Parcel app). Verified: typecheck clean, 72 suites / 644 tests pass, production build resolves all aliases, tsx scripts run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gs.json) Add typescript.tsdk = node_modules/typescript/lib so VS Code uses the project's TypeScript (the version CI runs) instead of its own newer, self-updating bundled TS. tsconfig sets compiler options, not the compiler version, so this is what makes the editor's Problems panel match CI. Documented in CLAUDE.md 2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g gate Take the lint forcing function to zero and promote it into ci-success (required): - eslint . --fix: import ordering + prefer-const across src/test (the bulk, ~750). - Calibrate rules to the project's existing conventions (in eslint.config.mjs): import/order to group-order only (no within-group alphabetize/newlines — the code groups aliased imports by subsystem), no-unused-vars to ignore ^_ (matches the tsconfig underscore convention), and a test-file override allowing require()/any (idiomatic jest mocking/fixtures). - Genuine code fixes: Field.ts &&-statements -> if; drop this-aliases in Person.ts and MainScene.ts (arrow handlers); no-explicit-any -> unknown in the type files, justified disables on the generic event bus + d3 interop; merge a duplicate import; add stable useEffect deps (game/city are stable props, selector strings stable-valued); and derive the family tree via useMemo instead of setState-in-effect (HouseDetails). - markdownlint: --fix the living docs; disable MD060 (new, cosmetic table-alignment) and ignore docs/tasks (frozen backlog) + docs/generated; add a language to bare code fences. - ci.yml: lint now BLOCKING (in ci-success needs). coverage stays advisory. Docs updated. Verified: npm run lint = 0, typecheck clean, 72 suites / 644 tests pass, build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es node globals Toolchain bumps to the newest versions the ecosystem supports: - typescript 5.4.5 -> 6.0.3. TS7 (7.0.2) is the native compiler and is NOT adoptable yet: ts-jest peers typescript <7 and typescript-eslint peers <6.1.0, so 6.0.3 is the newest both support. Moving to 7 would break tests (ts-jest) and TS linting (typescript-eslint) — deferred until upstream ships support. - ts-jest 29.4.0 -> 29.4.11 (29.4.0 peered typescript <6; 29.4.11 peers <7). - eslint stays 9.39.5 (latest 9.x): eslint 10 is blocked by eslint-plugin-react, which still peers eslint <10. markdownlint-cli2 already on latest (0.23.0). Regressions fixed: - TS 6.0 no longer auto-includes @types/* globals, so tests lost describe/expect and scripts lost Buffer/node: builtins. Declare them via tsconfig `types: ["node","jest"]` — this ALSO permanently fixes the editor's "Cannot find name 'node:fs'" errors in scripts/ (they resolve once the editor uses the workspace TS). CI's typecheck job already covers scripts/, so this class is caught in CI too. - params.ts: TS 6.0 stricter Array.includes typing — widen the drawable-arrangements array for the membership check. Docs: TS version in CLAUDE.md/README tech-stack + .vscode/settings.json comment. Verified: typecheck 0, eslint 0, markdownlint 0, 72 suites / 644 tests pass, build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a `perf` jest project + a dedicated CI job that protects the per-agent / subcomponent generation costs (the 078/079 strides) from regressing. It's unit perf testing — measure small parts so the sum stands in for the whole flow we otherwise never benchmark in CI. Runs in a few seconds, no coverage (istanbul would skew timings), --runInBand in CI. Two tiers: - regressionGuards.test.ts — DETERMINISTIC + within-run-RATIO guards, flake-free even under a loaded runner (the ratios/identity cancel machine noise, signals are 2-100×): invoke O(1) in pool size (agent-list gating), predicate precompilation stays a clear win, Inventory.contentsOf cache identity + invalidation, ActionEngine.contextFor memo identity + drop, and terminal action-instance pruning keeps state.instances bounded. These always enforce. - generationPerf.test.ts — the AGGREGATE per-agent / per-phase cost view via the 079 SubProfiler (covers every phase/hook/advance-sub-phase). Interleaved self-normalization (each run normalizes by its own calibration, min-of-R) makes it stable to ~5% run-to-run despite runner noise; gated at a loose 20% COST_TOLERANCE against committed baselines. ENFORCED only in the isolated CI job (PERF_ENFORCE=1); LOG-ONLY under a parallel `npm test` (sibling workers make the memory-bound sim diverge from the compute calibration). Machine-independence: absolute wall-clock can't gate reliably across CI runners, so the tight 5%-grade protection is the ratio/deterministic guards; the cost gate is the broad net. perfHarness.ts documents re-baselining (PERF_UPDATE_BASELINES=1) and how to promote the CI job from advisory to blocking (add to ci-success needs). CI: `perf` job runs on every change; advisory (not in ci-success needs) like `coverage`. jest.config.js registers the perf project OUTSIDE MODULE_COVERAGE so the coverage gate ignores it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follows the first CI run of the perf suite. Findings + response: - The deterministic + within-run-ratio GUARDS were byte-for-byte solid on CI (invoke pool-ratio ~1×, predicate cached/interpreted 0.35, pruning bounded) — they're machine-independent, so they now ALWAYS enforce and `perf` is added to `ci-success`'s needs (a BLOCKING check). This is the real protection of the 078/079 wins. - The aggregate cost gate is reworked from calibration-normalized absolute cost to per-phase FRACTIONS: each bucket as bucket / (total - bucket), its share of a tick, median over R fresh runs. Fractions are within-run ratios, so runner JITTER cancels — the calibration approach was defeated by a 12× swing in the CI runner's pure-compute calibration (2-vCPU VMs get descheduled), which deflated every bucket uniformly. - BUT the first CI run also showed fractions still drift ~14% across microarchitectures (the event-walk's share was ~14% higher on CI than a dev box), and ~15% under heavy differential load. So a dev baseline can't gate CI at 5-10%. The fraction gate is therefore LOG-ONLY for now (jitter-immune, so the logged trend is meaningful); enabling it needs CI-measured baselines (PERF_UPDATE_BASELINES on CI) + PERF_ENFORCE=1 on the job, documented in ci.yml and perfHarness. Net: perf is a strict, reliable BLOCKING check via the guards; the cost fractions are a jitter-immune trend log ready to enforce once CI-baselined. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Raise COVERAGE_THRESHOLD 72 -> 80 and add ~900 unit tests so every module clears it on its OWN files. Parallelized across per-module agents; each read the code and wrote behavioral/edge-case tests (not metric-gaming), keeping typecheck + lint green. Per-module owned-file statement coverage (all >= 80): execution 83.8, data 89.2, agents 92.8, economy 97.7, world 97.9, population 98.2, actions 98.3, events 98.5, save 98.6, history 98.8, util 99.4, objects 100, skills 100. Notable 0%/low files now covered: SkillRegistry, SocialLife, HousingMarket, LocalStorageProvider, PathFinder, Vehicle, City.setupHousehold + the monthly-economy chain. coverage-gate.mjs: filter each per-module report to the module's OWNED files before scoring. Jest's collectCoverageFrom is additive (it forces owned files in but doesn't filter transitively-required files out), so an unfiltered report diluted a module's number with other modules' code — the gate now measures only what the module owns. Coverage stays advisory for this push; flip to blocking (add `coverage` to ci-success) once CI confirms the gate green. Two real bugs found by the agents, pinned as regression tests (not fixed — out of the coverage task's scope): - PathFinder.getValidNeighbors indexes matrix[row] before the bounds check -> crashes on a start at the grid top/bottom edge (has a `// TODO: handle grid edges`). - Person.walk() clears currentTarget/currentDestination on arrival, so processTravel's immediate isDestinationReached() check reads cleared state (a false negative). Also: eslint --fix'd import order in the perf session's test/perf/*.test.ts so the blocking lint gate stays green for the shared branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aselines Enables the aggregate cost gate now that the first CI run proved fractions are machine-portable for the buckets that matter: - Baselines re-measured on CI (test/perf/baselines.json) — the dominant buckets landed within ±6% of a dev box there, the small (< ~0.05-share) ones within ±15%. - Only the DOMINANT buckets are ENFORCED (ENFORCED_FRACTIONS: phase.actions/ events/brain, brain.idleFallback, brain.freeTime:loop, advance.pool) at a 12% tolerance (headroom over the observed ±6% for CI-to-CI noise). A regressing sub-component still inflates its parent phase here, and the specific 078/079 wins are caught precisely by the deterministic guards; the small, noisy buckets are logged for trend but never fail (gateAgainstBaselines' `enforced` set, '*' in the table). - PERF_ENFORCE=1 on the CI perf job turns the gate on there; a dev run stays log-only (its fractions shouldn't gate against CI baselines). Validated: 3 enforce runs on a heavily-loaded dev box, gating against the CI baselines, all pass — the enforced subset holds where the raw wall-clock gate flaked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…udit on prod deps, make coverage blocking CI was red only on the advisory `audit` job. Fixes: - Remove the unused `app@0.1.0` dependency — nothing imports it and it solely dragged in mongoose@8.3.0 (the critical/high PRODUCTION advisories) plus a large junk tree. - `npm audit fix` (non-breaking) patched the remaining prod high (@babel/* transitive). - CI `audit` now runs `--omit=dev`: it gates the deps that actually SHIP. The remaining advisories are dev-only tooling (browser-sync's transitive ws/socket.io) that never reaches the bundle; `npm audit` locally still shows the full picture. Production audit at --audit-level=high is now clean (exit 0). Coverage: all 13 modules cleared 80% and the `coverage` check went green in CI, so it's now BLOCKING (added to `ci-success`'s needs) — the forcing function is locked in. (ci.yml also carries the other session's PERF_ENFORCE=1 change — shared file; verified its baselines are committed and the perf gate passes.) Verified: typecheck 0, build OK, prod audit exit 0, perf gate green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The second CI run showed the real CI-to-CI variance is ~±8-10% (phase.events 101.6% → 108.2%), not the ±6% a single dev↔CI comparison suggested — the two big phases (events, brain) trade tick time run to run. Anchoring the baseline to one run left that swing one-sided (+8.2% against a 12% gate = thin headroom). Re-center the six enforced baselines on the mean of both CI runs, so each run deviates ~±5% instead of +8/−0. Both observed CI runs now land within ±5.3% of baseline, leaving ~7% headroom under the 12% tolerance. The gate still trips well before a real regression (a lost 078/079 win balloons a phase far past 12%), and the precise per-optimization protection remains in the deterministic guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dance directives CLAUDE.md §5.3 now leads with HOW the tests are organized (one Jest project per module, test/<module>/ mirrors src/app/game/<group>/, util/ + perf/; `jest --selectProjects <module>`; alias imports; deterministic/seeded), and records the current gate status: per-module coverage is now BLOCKING (owned-file filter, 80%), alongside typecheck/build/ lint/perf; audit is advisory. Fixed the stale "coverage is advisory" notes in §2 and README. CLAUDE.md §5.1 task directives: - Every task ships with tests (behavior without covering tests = not done). - Every bug fix needs a regression test proven by a REVERT-DANCE — with fix + test green, `git stash push -- <file>` to undo only the fix, confirm the test now FAILS, then `git stash pop` and confirm it PASSES. A test that passes both ways guards nothing. Tooling hygiene: exclude `.claude/**` (worktree-isolated spawned-agent checkouts, gitignored) from markdownlint, ESLint, and tsconfig so local runs don't descend into the nested repo copies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the wall-clock cost-fraction gate (which drifted ~±8-10% across machines and needed centered baselines + a 12% tolerance) with EXACT operation counts. Given the sim's determinism, the counts are byte-identical on every machine and every run, so the gate demands exact equality — zero drift allowance — and any change in how much work a part of the sim does moves a count and fails, forcing a conscious PERF_UPDATE_BASELINES re-baseline. No PERF_ENFORCE flag, no CI-vs-dev caveat: it enforces the same everywhere. - util/perfMeter.ts: an ambient, opt-in counter. Probes call count(); with no active meter (all normal play + the whole offline generator) it's a single null check, and it reads nothing that affects logic or the RNG stream, so enabling it never changes output. 13 probes across predicate/ActionEngine/EventEngine/Brain/ Inventory/BootstrapWorld cover the spine's moving parts (predicate evals, active-index scan magnitude, context builds, event rolls, subject-eval gating, invoke pool scan, free-time computes, cache misses, co-location queries). - generationPerf.test.ts: meters a fixed spine window and asserts every count against baselines.json exactly, plus a determinism self-check (two runs → equal tallies) and the observable task-078 pruning invariant (instancesLive == agents). - regressionGuards.test.ts: the invoke O(1) guard is now an exact count (invokeScan == 0 at any pool size), not a timing ratio. The SOLE remaining timing check is predicate precompilation — counts can't see a same-work-slower regression — and it stays a machine-independent within-run ratio. Revert-dance: forcing invoke to always build the agent list flips invokeScan 0 → 9160, failing both the guard and the aggregate; restoring returns to green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Maudfer
added a commit
that referenced
this pull request
Jul 12, 2026
…g merge Merging main (PR #93 game/test reorg) moved PathFinder to game/agents/ and brought in the canonical test/agents/pathFinder.test.ts, whose pinned test asserted the OLD crash ("a start on the grid top edge crashes..."). The grid-edge fix (carried onto agents/PathFinder.ts by rename detection) makes that no longer throw, so update the pinned test to assert the fixed behavior — no throw AND a valid path found — and add a matching bottom-edge case (both use unpadded matrices, mirroring a real Field's top/bottom edge). Remove the now-redundant flat test/pathFinder.test.ts (added before the reorg was on this branch); its cases are folded into the canonical agents test, and the flat path no longer matches any Jest project's testMatch. Revert-dance (agents module): with the pre-fix index-before-bounds-check restored, exactly the top- and bottom-edge tests fail with "Cannot read properties of undefined"; with the fix, all 97 agents tests pass. agents/PathFinder.ts coverage 89.6% stmts (module 93.3%), above the 80% gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reorganizes the codebase into responsibility modules, splits CI into concurrent per-module checks, and consolidates docs. No behavior changes — a move/rename + config/docs refactor.
npm test(72 suites / 644 tests),typecheck,test:coverage,build-prod,validate-data, and both doc generators all pass.1 —
src/app/game/grouped by responsibility44 loose files moved into 11 subfolders (
scene/ world/ agents/ population/ events/ actions/ execution/ economy/ skills/ objects/ history/).GameManager.ts,City.ts,Clock.tsstay at the root (global orchestrators);data//save/unchanged. All alias imports remapped togame/<group>/<File>— thegame/*wildcard resolves it with no tsconfig/jest change.2 —
test/mirrors the grouping72 test files moved into 13 module folders (
test/<module>/,util/for pure utilities). All 586 relative../src/...imports converted to path aliases (CLAUDE.md §5.5 already mandated this; tests violated it), so test files are now location-independent.3 — Jest projects → concurrent, path-filtered CI
jest.config.jsdefines oneprojectper module;npx jest --selectProjects <m>runs one in isolation.ci.yml: achangesjob path-filters which modules a PR touched (shared/foundational code fans out to all), a dynamic matrix runs one concurrenttest (<module>)check per affected module, plustypecheck,build,audit, and acoveragejob.test (<module>)job emits its own report (the module measured by its own tests); thecoveragejob reads those reports (it does not re-run tests) and fails if any module is under a singleCOVERAGE_THRESHOLD(jest.config.js, currently 72% statements). Most modules are far below 72% in isolation today (the suite is integration-heavy) — intentional pressure to grow each module's unit tests. Thecoveragecheck is advisory (not inci-success, so it does not block merges) until modules climb; flip to blocking by addingcoveragetoci-success'sneeds.ci-successjob aggregates the required checks.4 — Docs
docs/generated/(generators, checked-diff tests,.gitattributes,package.jsonscripts updated; regenerated & verified).docs/simulation-flows.mdcondensed into new CLAUDE.md §4.14 "Simulation flows" and deleted.The required status check name changes. Repoint branch protection from
CI / build-and-testtoCI / ci-success, or PRs will block on a check that no longer exists. (TheCI / coveragecheck will be red by design — advisory forcing function — until per-module unit tests grow; make it required when ready.)🤖 Generated with Claude Code