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
2 changes: 1 addition & 1 deletion packages/core/src/loop/boringstack/gate-stages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<…, "<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}\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 EXACTLY match a generated key: the COLLECTION root carries a TRAILING SLASH (list/create are \`/api/v1/<resource>/\`, e.g. \`POST "/api/v1/<resource>/"\` — a POST/GET to \`/api/v1/<resource>\` WITHOUT the slash is the usual cause here; ADD it), while by-id is \`/api/v1/<resource>/{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,
};
}
Expand Down
17 changes: 11 additions & 6 deletions packages/core/src/loop/conventions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,17 +159,22 @@ const GUIDES: Readonly<Record<ConventionTopic, string>> = {
"`@/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 ' +
'`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 " +
"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/<resource>/"` and create is `POST "/api/v1/<resource>/"` (Elysia ' +
"mounts the group at `/api/v1/<resource>` and the handler at `/`, so the generated key is " +
'`/api/v1/<resource>/`). The by-id path has NO trailing slash: `"/api/v1/<resource>/{id}"` — use ' +
"the LITERAL `{id}` segment and pass the value via params, never string-interpolate the id. If a " +
"POST to `/api/v1/<resource>` (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 — " +
Expand Down
7 changes: 6 additions & 1 deletion packages/core/tests/boringstack-conventions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<resource>/"');
// 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");
Expand Down
3 changes: 3 additions & 0 deletions packages/core/tests/boringstack-gate-stages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down