diff --git a/packages/core/src/loop/boringstack/gate-stages.ts b/packages/core/src/loop/boringstack/gate-stages.ts index 4c7d7b3..f8153a2 100644 --- a/packages/core/src/loop/boringstack/gate-stages.ts +++ b/packages/core/src/loop/boringstack/gate-stages.ts @@ -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↳ \`PathsWithMethod<…, "">\` 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/\` (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}\n↳ \`PathsWithMethod<…, "">\` 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 EXACTLY match a generated key: the COLLECTION root carries a TRAILING SLASH (list/create are \`/api/v1//\`, e.g. \`POST "/api/v1//"\` — a POST/GET to \`/api/v1/\` WITHOUT the slash is the usual cause here; ADD it), while by-id is \`/api/v1//{id}\` (no trailing slash) with a literal \`{id}\` segment, value via \`{ params: { path: { id } } }\`, never interpolated; \`/api/x\` or \`/x\` (missing the \`/api/v1/\` prefix) is also wrong; (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, }; } diff --git a/packages/core/src/loop/conventions.ts b/packages/core/src/loop/conventions.ts index 29cbd0f..b44e21d 100644 --- a/packages/core/src/loop/conventions.ts +++ b/packages/core/src/loop/conventions.ts @@ -159,17 +159,22 @@ const GUIDES: Readonly> = { "`@/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/")`, NOT `"/"` or `"/api/"` (a wrong ' + + '`apiClient.GET("/api/v1//")`, NOT `"/"` or `"/api/"` (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 " + + "is a usage bug, not a stale-spec bug). TRAILING SLASH matters and is the #1 cause of a POST/GET " + + "`PathsWithMethod` on a path that otherwise looks right: the COLLECTION root carries a trailing " + + 'slash — list is `GET "/api/v1//"` and create is `POST "/api/v1//"` (Elysia ' + + "mounts the group at `/api/v1/` and the handler at `/`, so the generated key is " + + '`/api/v1//`). The by-id path has NO trailing slash: `"/api/v1//{id}"` — use ' + + "the LITERAL `{id}` segment and pass the value via params, never string-interpolate the id. If a " + + "POST to `/api/v1/` (no slash) is rejected, ADD the trailing slash. 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' + + '• list: `const { data } = await apiClient.GET("/api/v1/supplier/")` (collection → TRAILING SLASH)\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' + + '• query: `await apiClient.GET("/api/v1/supplier/", { params: { query: { status: "active" } } })`\n' + + '• create: `const { data } = await apiClient.POST("/api/v1/supplier/", { body: input })` (collection → TRAILING SLASH)\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 — " + diff --git a/packages/core/tests/boringstack-conventions.test.ts b/packages/core/tests/boringstack-conventions.test.ts index d12be41..5f6701a 100644 --- a/packages/core/tests/boringstack-conventions.test.ts +++ b/packages/core/tests/boringstack-conventions.test.ts @@ -133,13 +133,18 @@ describe("convention registry", () => { 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 })'); + // COLLECTION root carries a TRAILING SLASH (build14: the model sprayed on POST /api/v1/supplier + // for ~40 turns until it discovered the generated key is /api/v1/supplier/). + 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 } } })' ); + // The trailing-slash rule is stated explicitly (collection root vs by-id). + expect(g).toContain("TRAILING SLASH"); + expect(g).toContain('POST "/api/v1//"'); // 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"); diff --git a/packages/core/tests/boringstack-gate-stages.test.ts b/packages/core/tests/boringstack-gate-stages.test.ts index 2095950..ebb1b5f 100644 --- a/packages/core/tests/boringstack-gate-stages.test.ts +++ b/packages/core/tests/boringstack-gate-stages.test.ts @@ -157,6 +157,9 @@ describe("signatureToError", () => { expect(err.message).toContain("WRONG PATH"); expect(err.message).toContain("WRONG VERB"); expect(err.message).toContain("unregistered"); + // The steer must name the collection TRAILING SLASH (build14 endgame) — else the per-error + // feedback contradicts the front-loaded guide when the model is stuck on a slashless POST. + expect(err.message).toContain("TRAILING SLASH"); // …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.