Front rules and structure: one page file per route, lib/ for shared reads, leaner Room services, tests on the real route tree #408

Closed
opened 2026-10-15 12:31:51 +00:00 by root · 23 comments
root commented 2026-10-15 12:31:51 +00:00 (Migrated from gitlab.core-project.space)

Problem Statement

The front's structure rules cost more than they return, and some of them now fight TanStack's idioms:

  • Each route page is split into a .view.tsx (hooks, queries, mutations) and a .ui.tsx (layout). All 13 .ui.tsx files are imported only by their own view: no story and no test renders one alone. The split exists for a lint boundary. ADR 0011's own "revisit when" has fired: the Room view passes 24 props to its UI.
  • A route may import only a vertical's view or services/. So search validators live in views, and wrapper views exist only so a route can import a not-found screen.
  • The "no sibling import" rule pushed shared Account and My-games reads into lib/, which the rules call domain-free.
  • room/services/ holds 11 files, one per mutation, four of them under 20 lines, against root rules 18 and 19.
  • Three view-test harnesses rebuild 18 routes by hand, re-declaring loaders and search validators that already live in routes/, so a test can pass while the real route differs.
  • The front's standards still list four verticals; auth/ and games/ exist.

Source: the front audit, docs/research/front-audit.md, sections 2.3 and 3, and the owner's decisions in section 5 (view/UI merge accepted; cross-vertical reads stay in lib/ by rule).

Solution

  • The front's rules are rewritten as the owner decided: one page file per route, routes may import what they configure, lib/ may hold reads several verticals share, and a TanStack Form form or field object may be passed to a component.
  • Each route's view and UI become one page file. Reused layout stays in components/.
  • room/services/ shrinks to the files that each hold one concept.
  • View tests render the app's own route tree, so a route's loader and search validation are tested as they ship.

Nothing a Player sees changes.

User Stories

  1. As a maintainer, I want the front's rules to say what the owner decided, so that review and lint agree with the code we want.
  2. As a maintainer, I want one page file per route, so that a change to a screen touches one file instead of two.
  3. As a maintainer, I want a route file to import its page, its not-found screen and its search schema, so that route options live with the route as TanStack intends.
  4. As a maintainer, I want the rules to say that lib/ may hold reads several verticals share, so that the Account and My-games queries are where the rules expect them.
  5. As a maintainer, I want the lint config to stop requiring a <…Ui/> in every view, so that a page can render its own markup.
  6. As a maintainer, I want presentational components/ to keep their lint boundary, so that stories still render them without providers.
  7. As a maintainer, I want a component to accept a TanStack Form form or field object, so that the Form spec can bind design-system inputs once.
  8. As a maintainer, I want room/services/ grouped by concept (the Room query, the Room's mutations, the Move, entering a Room, refusals), so that one concept is one file.
  9. As a maintainer, I want the Move's mutation to stay in its own file, so that its lint exception for setQueryData stays narrow (ADR 0019).
  10. As a maintainer, I want view tests to render the app's route tree, so that a test exercises the loader and search validation that ship.
  11. As a maintainer, I want the hand-built test route trees gone, so that a route cannot drift from its test's copy.
  12. As a Player, I want every screen to look and behave exactly as before, so that the refactor is invisible.

Seams

  • S1 — the front lint plugin through its test (unit). Covers 1, 3, 5-7: front/view-is-composition is deleted with its test cases; the restricted-import globs accept the new page files and route imports, and still reject a sibling vertical and a presentational component importing Query.
  • S2 — every existing front view test, unchanged in what it asserts (front view tests). Covers 2, 8-9, 12: the merge and the services consolidation are refactors, and the existing tests are their proof.
  • S3 — the view tests rendered on the app's route tree (front view tests). Covers 10-11: the auth, games and home tests mount the real router with a memory history, keeping each vertical's MSW handlers and fakes.

No e2e seam: no Player-perceivable change.

Implementation Decisions

  • Rules, as one dedicated change the owner asked for (CLAUDE.md, "No rule edits in feature work"). In the front's CODING_STANDARDS.md:
    • lint rule ".ui.tsx and components/ import no Query…" keeps components/ only, and allows @/lib/form;
    • "a route imports only a vertical's *.view or services/" becomes "a route imports a vertical's page files, services/ and exported constants";
    • "a .view.tsx renders exactly one <…Ui/>" is deleted, with front/view-is-composition;
    • review rule 1 lists the current verticals; rule 2 says a file holds one concept, not one function; rule 3 becomes one page file per route; rule 4 exempts a TanStack Form form or field; rule 5 adds form state in TanStack Form; rule 8 says tests go through the page on the app's route tree;
    • lib/ and design-system/ still import no vertical, and the wording says lib/ may hold reads several verticals share.
  • ADR 0011 is amended, or superseded by a new ADR, for the page file and the lib/ wording; the ADR index row follows.
  • Page files. <name>.page.tsx (name to confirm in the rules change) per route, holding the hooks, the mutations and the layout. Not-found screens are exported beside their page.
  • Room services. The Room query with its invalidation; the Room's mutations (create, leave, resign, offers); the Move alone; entering a Room; refusals with their toast and write handler. The two announcement hooks become one.
  • Harnesses. Render the app's router with a memory history; each vertical's MSW worker and fake server stay. The shared hand-built route helper goes once nothing uses it.
  • Lint config size (1,192 lines, five blocks copied per vertical) is noted but not changed here.
  • ADRs: 0011 (amended), 0019 (the Move's exception kept), 0025, 0026.

Testing Decisions

  • Prior art: front-rules.test.ts for S1; the existing view tests for S2; app/router.harness.tsx (already mounts the real router) for S3.
  • A view test may change only how it mounts (S3) or what it imports after a rename (S2), never what it asserts.
  • Edge cases for S1: a page file importing Query passes; a components/ file importing Query fails; a route importing a sibling's internal file fails.

Out of Scope

  • TanStack Form (its own spec, which this one unblocks).
  • A shared account/ vertical (declined by the owner).
  • Generating the per-vertical lint blocks.
  • Moving the search schemas into route files (a follow-up once this lands, or folded into the merge ticket if it is small).

Further Notes

  • Audit: docs/research/front-audit.md on branch claude/nestjs-best-practices-audit-716d79.
  • This spec carries rule edits and the refactor they allow in one merge request. The owner asked for it, which is what CLAUDE.md requires.
  • Blocks #410 (TanStack Form v2).
## Problem Statement The front's structure rules cost more than they return, and some of them now fight TanStack's idioms: - Each route page is split into a `.view.tsx` (hooks, queries, mutations) and a `.ui.tsx` (layout). All 13 `.ui.tsx` files are imported only by their own view: no story and no test renders one alone. The split exists for a lint boundary. ADR 0011's own "revisit when" has fired: the Room view passes 24 props to its UI. - A route may import only a vertical's view or `services/`. So search validators live in views, and wrapper views exist only so a route can import a not-found screen. - The "no sibling import" rule pushed shared Account and My-games reads into `lib/`, which the rules call domain-free. - `room/services/` holds 11 files, one per mutation, four of them under 20 lines, against root rules 18 and 19. - Three view-test harnesses rebuild 18 routes by hand, re-declaring loaders and search validators that already live in `routes/`, so a test can pass while the real route differs. - The front's standards still list four verticals; `auth/` and `games/` exist. Source: the front audit, `docs/research/front-audit.md`, sections 2.3 and 3, and the owner's decisions in section 5 (view/UI merge accepted; cross-vertical reads stay in `lib/` by rule). ## Solution - The front's rules are rewritten as the owner decided: one page file per route, routes may import what they configure, `lib/` may hold reads several verticals share, and a TanStack Form `form` or `field` object may be passed to a component. - Each route's view and UI become one page file. Reused layout stays in `components/`. - `room/services/` shrinks to the files that each hold one concept. - View tests render the app's own route tree, so a route's loader and search validation are tested as they ship. Nothing a Player sees changes. ## User Stories 1. As a maintainer, I want the front's rules to say what the owner decided, so that review and lint agree with the code we want. 2. As a maintainer, I want one page file per route, so that a change to a screen touches one file instead of two. 3. As a maintainer, I want a route file to import its page, its not-found screen and its search schema, so that route options live with the route as TanStack intends. 4. As a maintainer, I want the rules to say that `lib/` may hold reads several verticals share, so that the Account and My-games queries are where the rules expect them. 5. As a maintainer, I want the lint config to stop requiring a `<…Ui/>` in every view, so that a page can render its own markup. 6. As a maintainer, I want presentational `components/` to keep their lint boundary, so that stories still render them without providers. 7. As a maintainer, I want a component to accept a TanStack Form `form` or `field` object, so that the Form spec can bind design-system inputs once. 8. As a maintainer, I want `room/services/` grouped by concept (the Room query, the Room's mutations, the Move, entering a Room, refusals), so that one concept is one file. 9. As a maintainer, I want the Move's mutation to stay in its own file, so that its lint exception for `setQueryData` stays narrow (ADR 0019). 10. As a maintainer, I want view tests to render the app's route tree, so that a test exercises the loader and search validation that ship. 11. As a maintainer, I want the hand-built test route trees gone, so that a route cannot drift from its test's copy. 12. As a Player, I want every screen to look and behave exactly as before, so that the refactor is invisible. ## Seams - **S1 — the front lint plugin through its test** (unit). Covers 1, 3, 5-7: `front/view-is-composition` is deleted with its test cases; the restricted-import globs accept the new page files and route imports, and still reject a sibling vertical and a presentational component importing Query. - **S2 — every existing front view test, unchanged in what it asserts** (front view tests). Covers 2, 8-9, 12: the merge and the services consolidation are refactors, and the existing tests are their proof. - **S3 — the view tests rendered on the app's route tree** (front view tests). Covers 10-11: the auth, games and home tests mount the real router with a memory history, keeping each vertical's MSW handlers and fakes. No e2e seam: no Player-perceivable change. ## Implementation Decisions - **Rules, as one dedicated change the owner asked for** (`CLAUDE.md`, "No rule edits in feature work"). In the front's `CODING_STANDARDS.md`: - lint rule "`.ui.tsx` and `components/` import no Query…" keeps `components/` only, and allows `@/lib/form`; - "a route imports only a vertical's `*.view` or `services/`" becomes "a route imports a vertical's page files, `services/` and exported constants"; - "a `.view.tsx` renders exactly one `<…Ui/>`" is deleted, with `front/view-is-composition`; - review rule 1 lists the current verticals; rule 2 says a file holds one concept, not one function; rule 3 becomes one page file per route; rule 4 exempts a TanStack Form `form` or `field`; rule 5 adds form state in TanStack Form; rule 8 says tests go through the page on the app's route tree; - `lib/` and `design-system/` still import no vertical, and the wording says `lib/` may hold reads several verticals share. - **ADR 0011** is amended, or superseded by a new ADR, for the page file and the `lib/` wording; the ADR index row follows. - **Page files.** `<name>.page.tsx` (name to confirm in the rules change) per route, holding the hooks, the mutations and the layout. Not-found screens are exported beside their page. - **Room services.** The Room query with its invalidation; the Room's mutations (create, leave, resign, offers); the Move alone; entering a Room; refusals with their toast and write handler. The two announcement hooks become one. - **Harnesses.** Render the app's router with a memory history; each vertical's MSW worker and fake server stay. The shared hand-built route helper goes once nothing uses it. - **Lint config size** (1,192 lines, five blocks copied per vertical) is noted but not changed here. - **ADRs:** 0011 (amended), 0019 (the Move's exception kept), 0025, 0026. ## Testing Decisions - Prior art: `front-rules.test.ts` for S1; the existing view tests for S2; `app/router.harness.tsx` (already mounts the real router) for S3. - A view test may change only how it mounts (S3) or what it imports after a rename (S2), never what it asserts. - Edge cases for S1: a page file importing Query passes; a `components/` file importing Query fails; a route importing a sibling's internal file fails. ## Out of Scope - TanStack Form (its own spec, which this one unblocks). - A shared `account/` vertical (declined by the owner). - Generating the per-vertical lint blocks. - Moving the search schemas into route files (a follow-up once this lands, or folded into the merge ticket if it is small). ## Further Notes - Audit: `docs/research/front-audit.md` on branch `claude/nestjs-best-practices-audit-716d79`. - This spec carries rule edits and the refactor they allow in one merge request. The owner asked for it, which is what `CLAUDE.md` requires. - Blocks #410 (TanStack Form v2).
root commented 2026-10-15 12:32:00 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #410

mentioned in issue #410
root commented 2026-10-15 12:32:00 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #410

marked this issue as related to #410
root commented 2026-10-15 12:32:08 +00:00 (Migrated from gitlab.core-project.space)

changed the description

changed the description
root commented 2026-10-15 12:33:45 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #414

mentioned in issue #414
root commented 2026-10-15 12:33:45 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #414

marked this issue as related to #414
root commented 2026-10-15 12:33:53 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #415

mentioned in issue #415
root commented 2026-10-15 12:33:53 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #415

marked this issue as related to #415
root commented 2026-10-15 12:34:07 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #416

mentioned in issue #416
root commented 2026-10-15 12:34:07 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #416

marked this issue as related to #416
root commented 2026-10-15 12:34:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #417

mentioned in issue #417
root commented 2026-10-15 12:34:09 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #417

marked this issue as related to #417
root commented 2026-10-15 12:34:20 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #418

mentioned in issue #418
root commented 2026-10-15 12:34:20 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #418

marked this issue as related to #418
root commented 2026-10-15 12:57:59 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !533

mentioned in merge request !533
root commented 2026-10-15 13:16:57 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 0818df5e7a

mentioned in commit 0818df5e7ae6f40d859114c9cbb5ff23be205ece
root commented 2026-10-15 13:17:12 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !534

mentioned in merge request !534
root commented 2026-10-15 13:27:14 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 7e2d65c99f

mentioned in commit 7e2d65c99fbefc639429af08f29381aac0d8adbf
root commented 2026-10-15 13:47:30 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 31b760f46b

mentioned in commit 31b760f46bd97cb5a45b24d1c0d41b11b8c8fc21
root commented 2026-10-15 14:14:07 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 9de3d0d7a6

mentioned in commit 9de3d0d7a6d90621232c4ecb5e099974ff11e0a9
root commented 2026-10-15 14:14:07 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit a37c5c0f71

mentioned in commit a37c5c0f71db9869d02b4146348737a9bce32341
root commented 2026-10-15 14:14:08 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 30b3ae6008

mentioned in commit 30b3ae60088885819c7c12403ce4c1db39ab7c94
root commented 2026-10-15 14:14:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 8ec9c87d13

mentioned in commit 8ec9c87d131775397020af09fca4b577a5dbb2ce
root commented 2026-10-15 15:10:31 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit f9f53ad52e

mentioned in commit f9f53ad52ecdbcda6735c366b76b69cc86aeb79e
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-15 15:17:48 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
amaurycoudr/chess-project#408
No description provided.