Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/core/src/loop/boringstack/gate-stages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,12 @@ export function signatureToError(sig: string): IErrorItem {
? " This `'>' expected` in a `.ts` is usually JSX in a non-JSX file — if it contains JSX, it must be a `.tsx` (a `.ts` parses `<X>` as a generic and demands `>`)."
: "";

// The api-client's types are GENERATED from the OpenAPI spec. Calling
// `apiClient.<M>("/api/v1/x")` for a path not yet in the spec fails typing with
// `PathsWithMethod<paths, …>` — the tell that the UI was written BEFORE the API route
// was registered + the client regenerated, NOT a UI-type bug to fight in the component.
// Steer to fix the ORDER, not the call site (observed: model ground on this + the
// coupled test-sibling for 20+ turns instead of touching the route).
// The api-client's types are GENERATED from the OpenAPI spec. A `PathsWithMethod<paths, …>`
// error means the path STRING doesn't match any generated key — which has TWO causes, and
// the steer must name both (observed live: build12's model used `/api/supplier` — a wrong
// CALL-SITE string, the route `/api/v1/supplier/{id}` was already in the spec — yet re-ran
// generate:api 5× because the old steer only ever said "route not in spec"). Cause 1 (check
// FIRST, cheaper): the path literal is wrong. Cause 2: the route genuinely isn't registered.
const isPathsWithMethod = message.includes("PathsWithMethod");

return {
Expand All @@ -143,7 +143,7 @@ export function signatureToError(sig: string): IErrorItem {
message: isParse
? `${message}\n↳ SYNTAX/PARSE error — do NOT surgically patch it (a patch on a broken-parse file re-breaks its braces/generics/JSX). REWRITE THE WHOLE FILE \`${file}\` cleanly in one pass; once it parses, downstream errors clear.${parseSteer}`
: isPathsWithMethod
? `${message}\n↳ This path is NOT in the OpenAPI spec yet — the api-client types are GENERATED from it, so don't fight the UI types. Make sure the API route exists AND is registered/mounted (so it shows in /swagger/json), then let the gate run — it re-runs generate:api and the path type appears. A \`/api/v1/…\` literal rejected by \`PathsWithMethod\` means the route isn't in the spec; fix the route/registration, not the call site.`
? `${message}\n↳ \`PathsWithMethod<…, "<verb>">\` means no generated key matches this path AND method. Three causes — check the call site FIRST (generate:api fixes NEITHER of the first two): (1) WRONG PATH string — the literal must be EXACTLY \`/api/v1/<resource>\` (every route is mounted under \`/api/v1/\` — \`/api/x\` or \`/x\` is wrong) with a literal \`{id}\` segment, value via \`{ params: { path: { id } } }\`, never interpolated; (2) WRONG VERB — the path exists but doesn't support this method (e.g. a GET-only path called with POST); call the method the route actually defines; (3) ONLY if path AND verb are already right is the route genuinely unregistered — ensure it exists AND is mounted (shows in /swagger/json), then the gate re-runs generate:api and the type appears. Do NOT re-run generate:api for a call-site (path/verb) bug.`
: message,
};
}
Expand Down
36 changes: 28 additions & 8 deletions packages/core/src/loop/conventions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,34 @@ const GUIDES: Readonly<Record<ConventionTopic, string>> = {
"the component rendering the field state the hook returns.",
"data-fetching":
"DATA-FETCHING (boringstack). ALL HTTP goes through the generated client " +
"`@/lib/api/client` — never `fetch`/`axios` (lint-banned). Call it as " +
'`const { data } = await apiClient.GET("/path")` (or `.POST`/`.PATCH`/`.DELETE` ' +
"with `{ body }`); `data` is typed from the OpenAPI spec. Errors THROW " +
"automatically via the client's `throwOnError` middleware (TanStack Query catches " +
"them) — so NEVER check `response.error`: it is typed `undefined`, so the guard is " +
"a dead `no-unnecessary-condition` gate error. Just read `data`. Put queries in " +
"`<Feature>.queries.ts` and mutations in `<Feature>.mutations.ts`. If a path isn't " +
"in the spec yet, add the API route first, then `bun run generate:api`.",
"`@/lib/api/client` — never `fetch`/`axios` (lint-banned). PATH STRINGS are the #1 thing " +
"the model gets wrong: the path is a LITERAL that must exactly match a key in the generated " +
"`paths` type, and every route is mounted under `/api/v1/` — so it is " +
'`apiClient.GET("/api/v1/<resource>")`, NOT `"/<resource>"` or `"/api/<resource>"` (a wrong ' +
"string is a `PathsWithMethod`/'not assignable' error that `generate:api` will NEVER fix — it " +
"is a usage bug, not a stale-spec bug). For a by-id path, use the LITERAL `{id}` segment and " +
"pass the value via params — never string-interpolate the id into the path. CALL SHAPE: the " +
"options object is OPTIONAL — a plain list GET is one arg (`GET(path)`); when you need params " +
"and/or a body they ALL go together in ONE options object as the SECOND arg — there is NEVER a " +
"third positional argument:\n" +
'• list: `const { data } = await apiClient.GET("/api/v1/supplier")`\n' +
'• by id: `const { data } = await apiClient.GET("/api/v1/supplier/{id}", { params: { path: { id } } })`\n' +
'• query: `await apiClient.GET("/api/v1/supplier", { params: { query: { status: "active" } } })`\n' +
'• create: `const { data } = await apiClient.POST("/api/v1/supplier", { body: input })`\n' +
'• update: `const { data } = await apiClient.PATCH("/api/v1/supplier/{id}", { params: { path: { id } }, body: input })`\n' +
'• delete: `await apiClient.DELETE("/api/v1/supplier/{id}", { params: { path: { id } } })`\n' +
"PATCH/PUT take path AND body in the SAME options object (one object as the second arg — " +
"passing body as a THIRD argument is an arity error: 'Expected 2 arguments, but got 3'). Errors THROW automatically via the " +
"client's `throwOnError` middleware (TanStack Query catches them) — so NEVER check " +
"`response.error`: it is typed `undefined`, so the guard is a dead `no-unnecessary-condition` " +
"gate error. Just read `data`; let TS infer its type (do NOT annotate a return type or `as`-cast " +
"it — index into it with a null-narrow, e.g. `data?.items ?? []`, to satisfy strict undefined " +
"checks). If `data` types as `Readable<SuccessResponse<…>>` instead of the JSON body, the API " +
"ROUTE is missing a single-content-type `response:` schema — fix the route (see api-service), " +
"not the consumer. Put queries in `<Feature>.queries.ts` and mutations in " +
"`<Feature>.mutations.ts`. If a path genuinely isn't in the spec yet, add the API route first, " +
"then `bun run generate:api` ONCE — if the error persists after one regen, the path STRING or " +
"call shape is wrong, not the spec.",
"lint-gotchas":
"STRICT-LINT GOTCHAS (boringstack). The gate is eslint with EVERY rule an error; a fresh " +
"feature trips these most, so write them right up front:\n" +
Expand Down
25 changes: 25 additions & 0 deletions packages/core/tests/boringstack-conventions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ describe("convention registry", () => {
expect(g).toContain("throwOnError");
});

test("data-fetching guide pins the exact api-client path format + call shapes (build12 park cluster)", () => {
const g = conventionGuide("data-fetching");

// The path bug that sprayed build12: model used "/api/supplier" — must be "/api/v1/…".
expect(g).toContain("/api/v1/");
expect(g).toContain("PathsWithMethod");
// Full method↔shape pairings (not loose fragments) — a fragment on the wrong method must fail.
expect(g).toContain('apiClient.POST("/api/v1/supplier", { body: input })');
expect(g).toContain(
'apiClient.PATCH("/api/v1/supplier/{id}", { params: { path: { id } }, body: input })'
);
expect(g).toContain(
'apiClient.DELETE("/api/v1/supplier/{id}", { params: { path: { id } } })'
);
// Options are OPTIONAL and there is never a THIRD positional arg (the corrected contradiction).
expect(g).toContain("options object is OPTIONAL");
expect(g).toContain("NEVER a " + "third positional argument");
expect(g).toContain("arity error");
// generate:api is the WRONG lever for a consumer/usage error (model reran it 5×).
expect(g).toContain("generate:api");
expect(g).toContain("usage bug, not a stale-spec bug");
// The Readable wrapper is a ROUTE response-schema problem, not a consumer fix.
expect(g).toContain("Readable<SuccessResponse");
});

test("testing guide teaches the exact idioms the model kept failing (extension, hoisted mock, route tests, rules)", () => {
const g = conventionGuide("testing");

Expand Down
22 changes: 14 additions & 8 deletions packages/core/tests/boringstack-gate-stages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,24 +141,30 @@ describe("signatureToError", () => {
expect(err.message).toContain("REWRITE THE WHOLE FILE");
});

test("a PathsWithMethod type error steers to REGISTER the route (not fight the UI types)", () => {
// Observed live (build6): model called apiClient.POST('/api/v1/supplier') before the
// route was in the OpenAPI spec → fought the UI types + coupled test-sibling for 20+ turns.
test("a PathsWithMethod type error steers to BOTH causes, call-site FIRST (build12: wrong /api/… string, route WAS in spec)", () => {
// build12: model used a wrong call-site string and re-ran generate:api 5× because the old
// steer only ever said "route not in spec". The steer must name the call-site cause first.
const msg =
"Argument of type '\"/api/v1/supplier\"' is not assignable to parameter of type 'PathsWithMethod<paths, \"post\">'.";
"Argument of type '\"/api/supplier\"' is not assignable to parameter of type 'PathsWithMethod<paths, \"post\">'.";
const err = signatureToError(
`failure:apps%2Fui%2Fsrc%2Ffeatures%2Fsupplier%2FSupplier.mutations.ts:12:no-unsafe:${encodeURIComponent(msg)}`
);

expect(err.message).toContain("NOT in the OpenAPI spec");
expect(err.message).toContain("register");
expect(err.message).toContain("generate:api");
// Call-site cause named first, with the exact /api/v1/ format.
expect(err.message).toContain("call site");
expect(err.message).toContain("/api/v1/");
// All three causes present: wrong path, wrong VERB (method-specific), unregistered route.
expect(err.message).toContain("WRONG PATH");
expect(err.message).toContain("WRONG VERB");
expect(err.message).toContain("unregistered");
// …and the wrong-lever warning: don't re-run generate:api for a call-site string bug.
expect(err.message).toContain("Do NOT re-run generate:api");
// A plain type error (no PathsWithMethod) is left untouched.
const plain = signatureToError(
"failure:apps%2Fui%2Fsrc%2Fx.ts:3:no-unsafe:Type%20'string'%20is%20not%20assignable%20to%20'number'."
);

expect(plain.message).not.toContain("NOT in the OpenAPI spec");
expect(plain.message).not.toContain("call site");
});

test("a `'>' expected` in a .ts flags the JSX-must-be-.tsx cause; other .ts parse errors do NOT", () => {
Expand Down