Skip to content

Latest commit

 

History

History
96 lines (76 loc) · 4.92 KB

File metadata and controls

96 lines (76 loc) · 4.92 KB

Architecture

Gitgotchi maps observable repo signals to a creature's state, and does it in a way that keeps the game logic trivially testable. The whole design follows from one rule: all I/O lives at the edges; the game is a pure function in the middle.

Data flow

collectors (async, I/O)   →   RepoSignals
RepoSignals + prev state  →   engine + decay (pure)   →   PetState
PetState                  →   store.save()  +  Ink render

One check-in (gitgotchi, one tick of watch, or --json) walks this exactly once, in src/core/session.tscollectAndAdvance:

  1. load the previous PetState (or a newborn).
  2. collectSignals runs every collector concurrently.
  3. tick advances the pet: computeVitals → applyDecay → computeMood, updates lifetime + stage, appends a history snapshot.
  4. save writes the new state atomically.

The layers

src/collectors/ — I/O in, RepoSignals out

One collector per signal source. The iron rule: collectors never throw. Any failure (missing file, weird git state, no network, rate limit) degrades to null or a safe default and logs only under GITGOTCHI_DEBUG.

File Signal
git.ts commit recency, dirty count, branch count, stale branches, ahead/behind
todos.ts TODO/FIXME/HACK counts (honors .gitignore via git ls-files, skips binaries)
coverage.ts coverage-summary.json (preferred) or lcov.info
ci.ts GitHub Actions status; token via GITGOTCHI_GITHUB_TOKENGITHUB_TOKENgh auth token; octokit is lazy-imported only when a token + remote exist
index.ts collectSignals — settles all four so one rejection can't poison the rest

src/engine/ — pure game logic

No I/O, no Date.now(), no unseeded Math.random(). The clock and seed are parameters, so running the engine twice on identical input is byte-identical (asserted by a determinism test). Held to 100% line coverage by a Vitest threshold.

File Responsibility
health.ts computeVitals(signals) → the four vitals
mood.ts computeMood(vitals, seed) → mood bucket + flavor line
flavor.ts mood → dry, non-preachy flavor lines (data)
decay.ts applyDecay(vitals, hours) — hunger/social erode while away
evolution.ts computeStage(lifetime, born, clock) + pickSpecies(key)

src/core/tick.ts composes these into the one pure tick() reducer.

src/state/ — schema-first persistence

  • types.ts / schemas.ts — the PetState contract, mirrored by zod schemas with a compile-time assertion that the two stay in lockstep.
  • store.tsload validates and migrates; on corrupt/invalid input it copies the broken file to state.json.bak and returns a newborn. save writes state.json.tmp then renames over the real file (atomic — a crash can never leave partial JSON). State lives outside the repo, so observing a repo never writes to it: GITGOTCHI_STATE_DIR, else $XDG_STATE_HOME/gitgotchi/, else ~/.local/state/gitgotchi/ (%LOCALAPPDATA%\gitgotchi\ on Windows). Each repo gets a <basename>-<sha256(abs path)[0..12]> slot, so two checkouts named api never collide. A pre-0.2 .gitgotchi/state.json is read once for migration and never written back.
  • name.tssafeName/stripControl scrub control bytes out of anything a user typed, at the persistence boundary and again at the SVG boundary. A pet name is rendered into a terminal and into a shared card; neither should ever see an escape sequence.
  • history.ts — a 50-entry snapshot ring buffer.

src/ui/ — the creature is the UI

Ink (React for terminals). Sprites are plain data (sprites/byte.ts), never inline strings, and are snapshot- and invariant-tested (every stage × mood is exactly two frames, ≤ 12×6, equal-width). App.tsx composes the sprite, HUD, and flavor line; launch.tsx drives one-shot and watch; plain.ts is the non-TTY fallback.

Health formula (v1)

  • hunger: 100 at ≤ 2h since last commit, linear to 0 at 7 days.
  • hygiene: 100 − 2·TODO − 5·FIXME − 8·HACK, floor 0.
  • social: 100 − 15·stale branches − (unpushed ? 10 : 0), floor 0.
  • health: with coverage → 70 + 30·coverage − 25·min(failStreak,3) − (failing ? 15 : 0); no CI/coverage → mean of the other three, so zero-config repos still work.
  • mood: lowest vital picks the bucket; all ≥ 70 → content; all ≥ 85 → thriving; any ≤ 15 → critical. Ties break health > hunger > hygiene > social.

Full fixture tables live in docs/milestones/M2-engine.md and the engine tests.