diff --git a/packages/core/src/agent/agent.constants.ts b/packages/core/src/agent/agent.constants.ts index 405058ed..95eec516 100644 --- a/packages/core/src/agent/agent.constants.ts +++ b/packages/core/src/agent/agent.constants.ts @@ -366,9 +366,11 @@ export const PULL_CONVENTIONS_TOOL = { "forms", "data-fetching", "lint-gotchas", + "testing", + "api-service", ], description: - "which guide: component-anatomy (where a component lives + one-per-file), file-layout (no inline types/constants/helpers), jsx (no computation in markup), state (hooks, not component body), no-casts (type guards instead of `as`/`!`), routing (thin route files), forms, data-fetching (api-client, never raw fetch), lint-gotchas (await promises, no void-expr values, no stringified errors, no duplicate strings).", + "which guide: component-anatomy (where a component lives + one-per-file), file-layout (no inline types/constants/helpers), jsx (no computation in markup), state (hooks, not component body), no-casts (type guards instead of `as`/`!`), routing (thin route files), forms, data-fetching (api-client, never raw fetch), lint-gotchas (await promises, no void-expr values, no stringified errors, no duplicate strings), testing (.test.ts vs .test.tsx, the vi.hoisted api-client mock, createApp/app.handle route tests, enforced test rules), api-service (mutating service methods record an audit event; throw ApiError).", }, }, required: ["topic"], diff --git a/packages/core/src/loop/conventions.ts b/packages/core/src/loop/conventions.ts index 1f6d0c12..ad9c5635 100644 --- a/packages/core/src/loop/conventions.ts +++ b/packages/core/src/loop/conventions.ts @@ -23,6 +23,8 @@ const TOPICS = [ "forms", "data-fetching", "lint-gotchas", + "testing", + "api-service", ] as const; export type ConventionTopic = (typeof TOPICS)[number]; @@ -60,18 +62,35 @@ export const TOPIC_RULES: Readonly> = "no-error-stringify", "no-duplicate-string", ], + // Tests were 61% of the edits on a live CRUD build (measured) — the model doesn't + // know the stack's test idioms so it flails: guesses .test.ts vs .test.tsx (and makes + // BOTH), reinvents the api-client mock (getting `any`-typed `data`), uses the wrong + // vi mock method. These rules fire on those mistakes; the guide teaches the idiom. + testing: [ + "test-sibling-required", + "test-file-mirrors-source", + "no-focused-tests", + "no-conditional-expect", + "no-real-network-in-unit-tests", + "fake-timers-must-be-restored", + ], + // The audit-event rule is a boringstack-OWN eslint rule (not a tsforge meta-rule), so it + // isn't keyed here for the reactive PUSH — the front-loaded guide is the delivery path. + "api-service": [], }; const GUIDES: Readonly> = { "component-anatomy": "COMPONENT ANATOMY (boringstack). A feature lives in `src/features//`. " + - "Components go under `src/features//components//`: `.tsx` " + - "renders props (it does NOT own state), and `index.ts` re-exports the default — " + - "ONE component per file. State/effects/memo live in `.hooks.ts`, never in " + - "the component body — the component imports the hook and consumes its return " + - "value. Feature-level files sit at `src/features//`: `.types.ts`, " + - "`.constants.ts`, `.queries.ts`, `.mutations.ts`. shadcn " + - "primitives in `src/components/ui/` are exempt.", + "Components go under `src/features//components//`, and component-folder-" + + "structure requires the FULL sibling set — create ALL of them or the gate rejects the " + + "folder ('missing required siblings'): `.tsx` (renders props, does NOT own state), " + + "`.hooks.ts` (all state/effects/memo — never in the body), `.types.ts` (its " + + "Props interface), `.stories.tsx` (a Storybook story — REQUIRED, easy to forget), " + + "`.test.tsx` (or `.test.ts`), and `index.ts` (`export { default as } from " + + '"./"`). ONE component per file. Feature-level files sit at `src/features//`: ' + + "`.types.ts`, `.constants.ts`, `.queries.ts`, " + + "`.mutations.ts`. shadcn primitives in `src/components/ui/` are exempt.", "file-layout": "FILE PURITY (boringstack). A component `.tsx` holds ONLY imports + the component " + "— nothing else atop it. Move each out and import it back: a type → " + @@ -84,7 +103,14 @@ const GUIDES: Readonly> = { "already-computed values. A derived value → a `useMemo` in `.hooks.ts`; " + "a pure transform → a function in `src/lib`. A simple ternary is fine; a " + "`.map()`/`.filter()`/arithmetic/`Object.entries()` in the markup is not (extract " + - "it). Every `