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 @@ -146,7 +146,7 @@ function structuredSteerMessage(message: string, file: string): string {
// emits json+multipart+text for EVERY route, scaffold ones included) — NOT a route/schema bug.
// The model burned ~60 turns (build15) chasing it on the API side; steer to the consumer.
if (message.includes("Readable<SuccessResponse")) {
return `${message}\n↳ \`Readable<SuccessResponse<…>>\` is the api-client's NORMAL, UNIVERSAL response type — Elysia's swagger emits three media types (json/multipart/text) for EVERY route (the scaffold's own auth/dashboard routes are identical), so you CANNOT remove it by editing the route or the \`response:\` schema — do NOT try. Fix it on the CONSUMER: the 'not assignable to Promise<IEntity>' error means you annotated the \`.queries.ts\`/\`.mutations.ts\` fn \`: Promise<IEntity>\` and did a bare \`return data\` — the fix is to REMOVE that return-type annotation and let TS INFER it (never \`as\`-cast). Then return the payload for THIS route's response shape: if it wraps \`{ data: … }\` (scaffold auth pattern) read \`data?.data\`; if it returns the object/array directly, just \`return data\` — match your \`response:\` schema, don't blindly add \`.data\`.`;
return `${message}\n↳ \`Readable<SuccessResponse<…>>\` is the api-client's NORMAL, UNIVERSAL response type — Elysia's swagger emits three media types (json/multipart/text) for EVERY route (the scaffold's own auth/dashboard routes are identical), so you CANNOT remove it by editing the route or the \`response:\` schema — do NOT try. Fix it on the CONSUMER by INFERRING, not annotating — it shows up two ways, BOTH fixed by removing an annotation (never \`as\`-cast): (a) \`not assignable to Promise<IEntity>\` = you annotated the \`.queries.ts\`/\`.mutations.ts\` fn \`: Promise<IEntity>\` with a bare \`return data\`; (b) \`UseMutationResult<Readable<…>> not assignable to UseMutationResult<IEntity>\` (or \`UseQueryResult\`) = you annotated the HOOK generic/return (\`useMutation<IEntity>\` / \`: UseMutationResult<IEntity>\`). REMOVE the annotation on the fn AND the hook and let TS INFER both. Then return the payload for THIS route's response shape: if it wraps \`{ data: … }\` (scaffold auth pattern) read \`data?.data\`; if it returns the object/array directly, just \`return data\` — match your \`response:\` schema, don't blindly add \`.data\`.`;
}

// A `PathsWithMethod<paths, …>` error means the path STRING doesn't match any generated key —
Expand Down
17 changes: 13 additions & 4 deletions packages/core/src/loop/conventions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export const TOPIC_RULES: Readonly<Record<ConventionTopic, readonly string[]>> =
"no-conditional-expect",
"no-real-network-in-unit-tests",
"fake-timers-must-be-restored",
// build16's final blocker (9× in one row-hook test): empty placeholder callbacks. The
// testing guide teaches `vi.fn()` for these, so map the rule here to re-push it reactively.
"no-empty-function",
],
// 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.
Expand Down Expand Up @@ -185,9 +188,13 @@ const GUIDES: Readonly<Record<ConventionTopic, string>> = {
"stream) for EVERY route — Elysia's swagger emits three media types (json/multipart/text) so " +
"openapi-fetch unions them. This is EXPECTED and UNIVERSAL (the scaffold's own auth/dashboard " +
"routes are identical); you CANNOT remove it from the route/response schema, so do NOT try. FIX " +
"IT ON THE CONSUMER. The error `Readable<…> not assignable to Promise<IEntity>` means you " +
"annotated the query/mutation fn `: Promise<IEntity>` and did a bare `return data` — the " +
"UNIVERSAL fix is to REMOVE that return-type annotation and let TS INFER it (never `as`-cast). " +
"IT ON THE CONSUMER by INFERRING, not annotating. The error appears two ways, BOTH fixed by " +
"removing a type annotation so TS infers from the unwrapped payload (never `as`-cast): " +
"(a) `Readable<…> not assignable to Promise<IEntity>` — you annotated the query/mutation fn " +
"`: Promise<IEntity>` and did a bare `return data`; (b) `UseMutationResult<Readable<…>> not " +
"assignable to UseMutationResult<IEntity>` (or `UseQueryResult<…>`) — you annotated the HOOK's " +
"generic/return type (`useMutation<IEntity>` / `: UseMutationResult<IEntity>`). Do NEITHER: " +
"leave the fn AND the hook UN-annotated and let TS INFER both. " +
"Then return the payload for YOUR route's response SHAPE: if the response wraps `{ data: … }` " +
'(the scaffold auth pattern), read `data?.data` — `const { data } = await apiClient.GET("/api/v1/' +
'supplier/"); return data?.data ?? [];`; if the route returns the object/array DIRECTLY (no ' +
Expand Down Expand Up @@ -253,7 +260,9 @@ const GUIDES: Readonly<Record<ConventionTopic, string>> = {
"• RULES the gate enforces: no `.only`/`.skip` (no-focused-tests); never put `expect` inside an " +
"`if`/loop/`switch`/ternary — assert unconditionally (no-conditional-expect); if you use fake " +
"timers, restore them in `afterEach` (fake-timers-must-be-restored); the test path/name mirrors " +
"its source (test-file-mirrors-source).\n" +
"its source (test-file-mirrors-source). A no-op/placeholder callback (e.g. an unused handler " +
"arg when testing ONE of a hook's callbacks) must be `vi.fn()`, NEVER an empty `() => {}` — an " +
"empty arrow/function body is a `no-empty-function` gate error (observed: 9 in one row-hook test).\n" +
"• After you write a test the harness AUTO-FORMATS it (imports reordered, quotes/commas normalized), " +
"so your next `edit` oldString won't match — RE-READ the file and copy oldString from its current " +
"content; do NOT recreate the whole file.",
Expand Down
11 changes: 10 additions & 1 deletion packages/core/tests/boringstack-conventions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ describe("convention registry", () => {
expect(g).toContain("data?.data");
expect(g).toContain("EXPECTED and UNIVERSAL");
expect(g).toContain("not assignable to Promise<IEntity>");
// build16: the SAME over-annotation habit also hits the HOOK generic (useMutation/useQuery),
// not just the fn return — the guide must name that facet too.
expect(g).toContain("UseMutationResult<Readable");
expect(g).toContain("HOOK's");
// The UNIVERSAL fix is infer-don't-annotate; data?.data is SHAPE-CONDITIONAL (panel: not
// every response is {data:…} — a raw object/array returns `data` directly), so the guide
// must NOT categorically prescribe .data.
Expand Down Expand Up @@ -202,21 +206,26 @@ describe("convention registry", () => {
expect(g).toContain("no-focused-tests");
expect(g).toContain("no-conditional-expect");
expect(g).toContain("fake-timers-must-be-restored");
// build16 final blocker: empty placeholder callbacks in tests → no-empty-function; use vi.fn().
expect(g).toContain("no-empty-function");
expect(g).toContain("vi.fn()");
// The auto-reformat re-read (the not-found edit-reject churn).
expect(g).toContain("AUTO-FORMATS");
});
});

describe("topicForRule (testing)", () => {
test("EVERY testing rule maps to the testing topic so its gate error pushes the guide", () => {
// Lock all six — a typo in any would silently disable the reactive PUSH for that error.
// Lock all — a typo in any would silently disable the reactive PUSH for that error.
for (const rule of [
"test-sibling-required",
"test-file-mirrors-source",
"no-focused-tests",
"no-conditional-expect",
"no-real-network-in-unit-tests",
"fake-timers-must-be-restored",
// build16: the empty-placeholder-callback rule must re-push the testing guide too.
"no-empty-function",
]) {
expect(topicForRule(rule)).toBe("testing");
}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/tests/boringstack-gate-stages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ describe("signatureToError", () => {
// Universal move is infer-don't-annotate; unwrap is shape-conditional (not blind .data).
expect(err.message).toContain("let TS INFER");
expect(err.message).toContain("don't blindly add");
// Steer must name BOTH annotation sites — the fn AND the useMutation/useQuery HOOK generic
// (build16 oscillated on the hook one; guide + steer must agree).
expect(err.message).toContain("UseMutationResult<Readable");
expect(err.message).toContain("HOOK generic");
// Must NOT tell the model to fix the route/schema for this.
expect(err.message).toContain("CANNOT remove it by editing the route");
});
Expand Down