--- name: odc-web-route description: Add or change a route in apps/web — a page under src/routes, its loader, or the useXQuery / useXMutation hook it prefetches. Use also when tsc reports TS2345 on a route literal, or when route-tree.ts or the Playwright suite's RouteTo union is missing it (gen:routes). --- A route file is the one kind of source in this repo that two **generated** artifacts are derived from, and the app you just edited regenerates neither. Until both catch up, a perfectly correct route fails `tsc` in two different workspaces. ## Two generated artifacts, two owners | Artifact | Derived from | Regenerated by | | -------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `apps/web/src/route-tree.ts` (git-tracked) | every file under `apps/web/src/routes/` | **the user**, by running `pnpm dev` for web — any vite serve or build of `apps/web` runs the `tanstackRouter` plugin in `apps/web/vite.config.ts`, which writes the file | | `testing/src/generated/route.d.ts` (ignored) | `FileRouteTypes['to']` in `route-tree.ts` | **you**: `pnpm --filter @opendatacapture/testing gen:routes` | **The user carries the baton first.** `gen:routes` derives `RouteTo` from `route-tree.ts` (`testing/AGENTS.md`), so running it against a stale tree writes the same file back: your literal is still not a member of `RouteTo`, and the `pageModels` map in `testing/src/support/fixtures.ts` still rejects it as a key. Ask the user for the regeneration, then run `gen:routes` again — repeating it costs nothing. The root `AGENTS.md` prohibition covers the first row only. `gen:routes` is not the route-tree generator — it reads `route-tree.ts` and never writes it — and running it is your job, not the user's. **TS2345 on your own route literal is then the expected state of a correct new route**, not a defect to chase; `.agents/docs/playbooks/add-web-route.md` shows the exact message. Confirm first that the `createFileRoute` literal is character-for-character the file path, leading `/_app` included — a mismatched literal raises the same error and survives regeneration. Once it matches, carry on with the rest of the change and leave both `route-tree.ts` and its generator to the user, unedited (root `AGENTS.md` hard rule). Done when your reply names the regeneration the user still owes, and either every literal you wrote appears in `testing/src/generated/route.d.ts` or your reply says it cannot yet. ## Where the procedure lives Open the playbook before you write the file; it is the order of operations, and this skill does not repeat it. | Open | When | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.agents/docs/playbooks/add-web-route.md` | Writing or moving a file under `src/routes/` — filename-to-URL mapping, the `createFileRoute` literal, `validateSearch`, `loaderDeps`, the nav entry | | `.agents/docs/playbooks/add-web-data-hook.md` | The data the route needs has no hook yet, or you are changing one — every input that changes the response belongs in the `queryKey` | | `apps/web/AGENTS.md` | Before naming any route file — a `.` is a path separator, so a stray `logs.test.tsx` becomes the route `/logs/test` — and for the rest of this app: layer folders, the store, translations, where a test may live | ## One key, two callers The loader prefetches through the hook's `queryOptions` factory and the component calls the hook (`add-web-data-hook.md` step 3). **Sharing the factory is half of it — the arguments must match too.** `apps/web/src/routes/_app/datahub/index.tsx` passes `{ groupId: currentGroup?.id }` on both sides and reaches one cache entry. `_app/admin/audit/logs.tsx` does not: the loader prefetches `deps.search`, the component asks for `{ ...search, page, sortOrder }`, and `useAuditLogsQuery` keys on the whole params object — so the prefetched entry is never read and the page fetches again on mount. Done when every `ensureQueryData` call in your loader passes both the factory and the arguments the component's hook passes. ## Before you leave Add a `data-testid` to whatever the e2e test will click or assert on, then open `.agents/docs/playbooks/add-e2e-test.md` before writing the spec — it owns the selector contract and the fixture registration. The unit test goes in a `__tests__/` folder beside the file that changed, named after it: `src/hooks/__tests__/` for a query hook, `src/routes/**/__tests__/` for the route itself. The playbook's `## Verify` block is the check to run, with one ordering caveat: `pnpm test:e2e` boots `apps/web` through `pnpm dev:test` (`testing/playwright.config.ts`), which regenerates `route-tree.ts` as a side effect. Run it once the user has regenerated, when it rewrites the file to the same content and leaves no diff. Done when every element the spec selects carries a `data-testid`, the unit test file exists, and the Verify block has run clean apart from TS2345 on your own route literal.