Skip to content

Latest commit

 

History

History
143 lines (123 loc) · 8.58 KB

File metadata and controls

143 lines (123 loc) · 8.58 KB

CLAUDE.md

Guidance for working in this repo. Day One is a phone-first reentry simulator — an installable, offline React PWA where you live the first ~90 days after release, one weekly turn at a time. Read docs/DESIGN.md (the source of truth) before non-trivial changes; docs/ABOUT.md is the plain-language overview.

Commands

npm run dev          # compile+validate content, then Vite dev server (:5173)
npm run build        # tsc -b + content build + production PWA build → dist/
npm run preview      # serve the production build (:4173) — needed to test PWA install/offline
npm test             # Vitest: engine + per-sprint + end-to-end playthrough tests
npm run typecheck    # tsc -b (regenerates the corpus first via pretypecheck)
npm run build:content  # just the YAML→JSON content pipeline
npm run icons        # regenerate the procedural sunrise PWA icons

The service worker is off in dev — use build && preview to test install/offline. src/content/corpus.generated.json is generated (gitignored); dev/build/test/ typecheck all regenerate it. Editing content/, schema/, or the flag registry in dev recompiles + reloads.

Architecture (one direction of dependency)

content (data) → engine (rules) → UI (React). The engine and content know nothing about React.

  • src/engine/ — pure functions over plain, JSON-safe state. No React, no I/O, no globals. engine.ts (turn loop, choice resolution, effects, scheduling, obligations, the monthly economy tick, serialization), chargen.ts (origin → opening state), randomchar.ts (deterministic random origins), predicate.ts (safe evaluator), rng.ts (seedable mulberry32), debrief.ts (ending profile + trajectory + decision-quality + mode framing), tuning.ts (balance knobs), flags.{ts,json} (flag registry), types.ts (canonical contracts — keep authoritative). index.ts is the public barrel the UI imports from.
  • content/ — events + character origins as YAML. scripts/compile-content.mjs validates each event against schema/event.schema.json (AJV) and fails the build on invalid content or an unknown flag. Compiles to the corpus the app imports and the SW precaches.
  • src/ui/ + App.tsx — mobile-first, single-column, accessible React shell. Renders state, collects taps. Dexie (db.ts) autosaves/resumes a run; theme.ts does light/dark + accent; scenario.ts does the character.mode.seed classroom codes; runShare.ts does full run export.

Engine systems (where to look)

  • Turn loop (engine.ts): beginTurn replenishes slots (base 7 − standing commitments), fires scheduled incidents into pending, applies weekly flows (wage while has_job; a few-week unemployment bridge after a layoff; the weekly home-detention fee), and runs the monthly economy tick (turns 4/8/12): benefits stipend in, and the drains out — transit pass, phone plan (both drop the asset if unpaid), probation supervision fees, and child support for a reunifying parent. endTurn enforces missed obligations, fires edge-triggered pool-floor crises (money/health/morale via CRISIS_TRIGGERS), snapshots pools, and handles the gated terminal chain. A missed obligation schedules a violation event chosen by supervision type (violationEventFor: parole, probation, or home detention).
  • Scoring (debrief.ts): ending profile + milestones + trajectory (pool-vitality momentum) + decision quality (durable vs desperate, from per-choice quality tags recorded in the log) + mode-aware framing.
  • Reproducibility: a run = character + mode + seed. createRunFromOrigin chargens any origin (corpus or randomOrigin(seed)); same inputs ⇒ identical run.

Non-negotiables (if a change conflicts with these, the change is wrong)

  1. Engine purity. Nothing in src/engine/ may import React or do I/O. Mutating functions clone (structuredClone) and return new state.
  2. Determinism + serialization. All randomness flows through state.rngState via rng.next. serializeRun/loadRun must round-trip. New GameState fields must be JSON-safe and defaulted in loadRun (save migration).
  3. Barriers are data. Every obstacle is a requires on a choice or a condition on an event — never hardcoded in the engine or UI. The catch-22 is a visible, disabled button.
  4. No "you lost" screen. Setbacks are crises with branches; the run is scored on trajectory and decisions, not just final position. Terminal endings are rare, gated (accumulated violations + hardFail), and handed to the debrief.
  5. Two audiences, one engine. Training vs empathy differ only in onboarding, difficulty defaults, debrief framing, and the hardFail flag — never in core rules.

Conventions

  • Flags live in src/engine/flags.json (the single registry). Referencing a flag in content that isn't registered fails the content build. Exception: evt_<id>_unlocked flags are generated by an outcome's unlocks. Add new flags there before using them.
  • Predicate grammar (conditions/requires): single comparisons, AND-ed, no boolean operators, no eval. Paths: flags.X | pools.X | tracks.X.status | tracks.X.readiness | turn | mode | config.X. Unknown flags read false.
  • Effects: pool deltas clamp to [0,100]; track status is set absolutely, readiness is a clamped delta (exception: housing readiness is kept in sync with the status as the ladder rank — HOUSING_RANK in tuning.ts — so housing rungs gate on tracks.housing.readiness < N and only move up); unlocks sets <id>_unlocked; schedule fires a future incident; flags set booleans.
  • Tuning numbers go in src/engine/tuning.ts, not scattered in logic.
  • Tests are colocated in src/engine/*.test.ts, one file per sprint plus the end-to-end playthrough.test.ts. Add tests for new engine behavior and content gating. Note: buildDebrief is exported from ./debrief (and the ./index barrel), not from ./engine.ts — import it accordingly in tests.

Adding content (the common task)

  • An event: copy an existing file in content/events/, edit prose/choices/ effects, keep the id matching the filename (evt_[a-z0-9_]+). Register any new flags. Run npm run build:content — it fails loudly on mistakes. Events self-gate via conditions; mark repeatable: true for things revisitable across weeks (the engine also limits each event to once per turn).
  • A character: add an origin YAML in content/characters/, then add its id to ARCHETYPE_ORDER in src/content/corpus.ts. chargen maps origins to state generically (pools, tracks, flags, schedule) — author difficulty in the origin data, not in code. Several barrier flags are derived by chargen, not authored: registry_required (from offense.registry_required), tech_gap (long time inside ≥ TECH_GAP_YEARS), chronic_mental_health (from person.mental_health_issue), reunifying (from person.reunifying), owes_supervision_fees (from a supervision_fees condition), owes_home_detention_fees (from a home_detention condition), and has_phone/owes_phone_plan + has_laptop (from landing.has_phone / landing.has_laptop). Standing slot taxes come from supervision conditions (mandated_treatment, community_service, home_detention).
  • Difficulty/mechanics knobs (slot tax, transport multiplier, crisis floors, monthly economy amounts, terminal threshold, tech-gap years) live in src/engine/tuning.ts.

Workflow & deploy

main is protected — no direct pushes. Work on a branch, open a PR, let CI (.github/workflows/ci.yml: content validation + typecheck + tests + build) go green, then merge. Self-merge is allowed (0 required approvals). See CONTRIBUTING.md.

Production is live on DigitalOcean App Platform (static site) from main with deploy_on_push on, so merging a PR to main auto-builds and redeploys. Spec: .do/app.yaml. The app is a static PWA, so it also self-hosts behind nginx or Docker. Full paths + the DNS gotcha we hit: docs/DEPLOYMENT.md.

Gotchas

  • Don't commit src/content/corpus.generated.json (gitignored; regenerated).
  • Never push directly to main — it's protected, and landing a change there deploys to production. Branch → PR → green CI → merge. Keep it green locally first (npm run typecheck && npm test && npm run build).
  • Node is via nvm here; if a tool reports "npm not found" in a non-interactive shell it's a PATH/nvm issue, not a repo problem.