Skip to content

refactor: group the client by feature and give the backend one source for its rules - #34

Merged
ishaanko merged 2 commits into
mainfrom
refactor/slop
Oct 29, 2026
Merged

ishaanko merged 2 commits into
mainfrom
refactor/slop

Conversation

@ishaanko

Copy link
Copy Markdown
Owner

The codebase had a lot of slop: the client was split by layer (components/, pages/, lib/, data/), so one mode lived in four folders; Daily.tsx and ui.tsx were god files; game numbers and validators were copied between the schema, the rules and the page copy; jobs carried v.any() args that the worker API cast back; and some arguments were never checked at the API boundary.

Now the client has a folder per mode (daily/, dex/, bounties/, lab/, map/, plus home/, learn/, players/), with shared neuron widgets in neuron/ and generic primitives in ui/. The backend has one source for each rule, and bad states and bad input can't get in.

  • Layer-first folders: src/ is grouped by feature. worker/places.py now writes src/map/zones.json.
  • God files: Daily.tsx (486 lines) is split into the page, result card, guess list, heat review and archive. ui.tsx is split into generic ui/ primitives and neuron/ parts.
  • Duplication: the effect and rarity validators come from rules.ts. Profile and puzzle fields are declared once and reused by the worker API. Heat bands have one threshold table, used by the bar, chart, map and share grid. The example game reuses the daily's guess row. Lab's Metric became Stat. checkPush and profileOf replace repeated inline code. PageTitle, DoseInput and buttonClass replace copied markup. JoinDialog uses Modal.
  • Representable illegal states: rarity is a union, not a string (removes 6 casts). Jobs are a union per kind, so job.kind === "judge" gives typed args. daily.current nests answer inside result, and lab.mine returns result only once one exists, which removes the ! assertions in Daily and Lab.
  • Layering and pass-throughs: lib.ts no longer re-exports rules.ts. The client no longer imports convex/bountyPool (where the hidden judge prompts live). lab.ts no longer imports DOSE from bounties.ts. Stats moved out of jobs.ts into stats.ts. lib/ no longer imports components. The session/JoinDialog import cycle is gone.
  • Implicit coupling and shotgun surgery: dose, efficiency, bounty length and reply length live in rules.ts, and the copy reads them. About and the field notes read the placed and quiet counts from zones.json. Clerk and the theme-color meta read theme colors from the CSS instead of keeping copies. Nav and footer links sit together.
  • Validation at the boundary: malformed dates, search layers and map squares are refused where they come in, and catchNeuron checks its neuron. /daily/<garbage> shows "No such puzzle." Saved field notes are filtered to known ids.
  • Worker: the four copy-pasted job loops are now one dispatch, and the sweep cursor path reuses STORE.

Deploy notes

  • The schema is stricter: rarity and the job rows. Convex checks existing rows when it deploys. If an old prod row doesn't match, the deploy fails and the current version stays live; no data is touched. The local dev data passed.
  • Two query shapes changed (daily.current, lab.mine). A tab left open across the deploy may break on a finished daily until it reloads.
  • Worker changes take effect only after a pull and restart of neuronaut-prod-worker. I didn't restart it.

Verification

  • bunx tsc -p convex --noEmit, bun run build, bun run lint (20 warnings, down from 23) and bun run test (81) pass. Worker pytest passes (29).
  • Pushed to a copy of the local dev backend, then walked every page in Chromium with no page errors. Played practice puzzle fix: reject non-finite bounty steering doses #1 to a solve as a guest; the result card, the solved panel, the answer on the map and "Why each guess scored" all render from the new result.answer.
  • Checked the worker dispatch with stubbed handlers: same order as before (judge, par, daily, combo), and failures still go to worker:fail. Not run on the GPU.
  • e2e/session/run.mjs fails all 15 cases on main too: its about:blank page has no crypto.randomUUID. With a polyfill added to a local copy, it passes 15/15 on both main and this branch. The full e2e/game.mjs wasn't run, because it needs a second GPU worker next to the prod one.

A follow-up design PR is stacked on this branch.

Claude Opus 5.5 in Claude Code

🤖 Generated with Claude Code

… for its rules

The client was split by layer (components/, pages/, lib/, data/), so one mode's code sat in four folders. It now
has a folder per mode (daily/, dex/, bounties/, lab/, map/) plus home/, learn/ and players/, with neuron widgets in
neuron/ and generic primitives in ui/. The 486-line daily page is split into the page, its result card, the guess
list, the heat review and the archive, and the example game reuses the daily's guess row.

Backend:
- schema validators come from the rules (effects, rarities) instead of copies, and rarity is a union, not a string,
  so the casts to Rarity are gone
- jobs are a union per kind, so the worker API reads typed args instead of casting from v.any()
- daily.current nests the answer inside the result, and lab.mine returns a result only once one exists
- shared checks live in lib.ts (checkPush, checkDate, profileOf) and lib.ts no longer re-exports rules.ts
- dates, the search layer, the map square and the catch neuron are checked where they come in
- dose, efficiency, bounty length and reply length live in rules.ts, so the client copy reads them instead of
  repeating the numbers, and the client no longer imports the server-only bounty pool
- the stats row has its own module instead of living in jobs.ts
@greptile-apps

This comment has been minimized.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines, tests and lockfiles excluded. vouch:trusted Author has write access or is in .github/VOUCHED.td. labels Oct 29, 2026
@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the reported search crash is fixed.

Summary

This PR groups the client by feature, shares backend rules and validators, and tightens input checks.

  • The backend checks player inputs against shared rules before using them.
  • Each GPU job kind now carries its own argument shape through the worker.
  • The daily game shows its answer inside the finished result.
  • Client code now lives beside the feature that uses it.
  • Map pages and their generated zone data now share a feature folder.

Reviews (2) · Last reviewed commit: "fix(web): don't send the lens search a l..." · Reviewed by Greptile

Comment thread convex/neurons.ts Resolved
The backend now refuses it, and a refused query throws while the page renders.
@ishaanko
ishaanko added this pull request to stack #36 October 29, 2026 20:26
@ishaanko
ishaanko merged commit 65bec7a into main Oct 29, 2026
7 checks passed
@ishaanko
ishaanko deleted the refactor/slop branch October 29, 2026 20:30
ishaanko added a commit that referenced this pull request Oct 29, 2026
…on paper (#35)

Stacked on #34. Merge that first, and GitHub will retarget this one to
`main`.

The UI was full of design slop: a double-ruled "field plate" with corner
registration marks, a `Plate Nº 003` strip and a `daily / 2026-10-29 /
easy` breadcrumb, ASCII textures inside bordered panels, dividers
between every row and section, a translucent blurred nav and modal
backdrop, five neutral shades plus opacities, italic lines, tight
headline kerning, letter-spaced mono capitals over every section, rarity
pills, a numbered how-to list, a heat chart with a ruler, legend and
curve, and the behavior map repeated on four pages.

Now the content is plain type on paper, grouped by space:

- **Palette:** `bg`, `fg`, `dim` and one `line` color, used only for
input edges (3.3:1 on paper, 3.8:1 at night). `card`, `raised`, `faint`,
`line-strong` and the accent shades are gone, and so is every `/NN`
opacity and the blur. Rarity and heat keep their colors because they
carry meaning.
- **Containers and dividers:** no bordered boxes, cards, panels, plates
or row rules anywhere. Buttons are square and swap ink and paper on
hover. Inputs keep one edge.
- **Type:** no italics (the italic font file is dropped too), no
`tracking-tight`, no letter-spaced capitals. Section headings are plain
serif. Page titles lose the rust full stop, and the headline is one
color.
- **Numbering and breadcrumbs:** no "Plate Nº", "Daily Nº" or "Field
guide" lines above headings, and no path strips. The how-to steps are
paragraphs. The puzzle keeps its `#3`, since that's its name in the
share text.
- **Tags:** rarity is a colored word with no dot, and the specimen loses
its glitch tag.
- **Diagrams:** the heat chart is gone. The guess list already shows
each heat, and the result card now shows the share grid. The behavior
map stays only on the Map page; the Dex, neuron and daily copies are
gone. The Map page loses its on-map labels (the legend beside it names
the zones) and per-dot colors.
- **Modal:** a plain full-page sheet with a Close button inside the
dialog, instead of a framed box over a dimmed page.
- `daily.current` no longer looks up map places for each guess, since
nothing draws them.
- `CONTRIBUTING.md`'s UI rule now lists these, so they don't creep back.

The zone colors on the map were drawn at 32% opacity, but the legend
swatches were full strength, so the two never matched (that's true on
main too). Both now use the same solid mix of the zone color and the
page color.

This reverses the settled field-plate look (frame, corner marks, mono
capitals, ASCII fields). Cream paper stays the default, and dark stays
opt-in.

**Verification**
- `bunx tsc -p convex --noEmit`, `bun run build` (bundle 12 KB smaller),
`bun run lint` (20 warnings, same as #34) and `bun run test` (81) pass.
- Ran it against a copy of the local dev backend and walked every page
in Chromium in both themes at 1360 px, and in paper at 390 px, with no
page errors. Played practice puzzles to a solve as a guest: the result
card, the solved panel and "Why each guess scored" all render.
- `e2e/session/run.mjs` passes 15/15, with the `crypto.randomUUID`
polyfill described in #34; the join dialog now uses the new modal.
`e2e/game.mjs` wasn't run (it needs a second GPU worker), but I checked
the selectors it uses still exist.
- Before and after screenshots are local, not attached.

Claude Opus 5.5 in Claude Code

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines, tests and lockfiles excluded. vouch:trusted Author has write access or is in .github/VOUCHED.td.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant