From 2abe5ce8b69da6df3a86bc09043c83217f614d9a Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Sun, 5 Jul 2026 02:35:23 -0500 Subject: [PATCH] =?UTF-8?q?fix(scaffold-core):=20generated=20project=20typ?= =?UTF-8?q?echecks=20clean=20=E2=80=94=20tsconfig=20lib=20pin,=20Contentfu?= =?UTF-8?q?lStatusCode,=20typed=20body=20catch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E production test (generate -> npm install -> tsc --noEmit) on the merged 1.8.0 candidate caught three residual template defects: - generated tsconfig omitted "lib", so tsc injected lib.dom which collides with @cloudflare/workers-types globals; pin lib: ["ES2022"] + skipLibCheck, matching the materializer's renderTsConfig - HttpError.status was number; Hono's c.json(body, status) requires ContentfulStatusCode, so the generated app.onError failed typecheck - route templates' .json().catch(() => ({})) widened the body union to {}, breaking property access under strict Both representative archetypes now install + typecheck + run generated tests clean from a cold download. Regression guards in output-quality.test.ts. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 1 + .../src/__tests__/output-quality.test.ts | 26 +++++++++++++++++++ packages/scaffold-core/src/codegen/files.ts | 10 +++++-- packages/scaffold-core/src/codegen/routes.ts | 8 +++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fc1c8..66f6d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on Keep a Changelog and follows Semantic Versioning. ### Fixed +- **`@stackbilt/scaffold-core` generated project now typechecks clean out of the box** (`stackbilt-web#151` follow-through) — E2E production test (generate → `npm install` → `tsc --noEmit`) caught three residual template defects after the traitMap fix: (1) generated `tsconfig.json` omitted `lib`, so tsc injected `lib.dom` which collides with `@cloudflare/workers-types` globals (~40 declaration-conflict errors) — now pins `lib: ["ES2022"]` + `skipLibCheck: true`, matching the materializer's `renderTsConfig`; (2) `HttpError.status` was `number` but Hono's `c.json(body, status)` requires `ContentfulStatusCode`, so the generated `app.onError` handler failed typecheck — now typed as `ContentfulStatusCode`; (3) route templates' `.json().catch(() => ({}))` widened the body union to `{}`, breaking property access under `strict` — catch fallbacks now typed as `{} as T`. Regression guards added to `output-quality.test.ts`. - **`@stackbilt/scaffold-core` hero-demo output quality** (`tarotscript#427`, `stackbilt-web#151`) — `classify()` computed a `traitMap` (route_shape, default_routes, source_pattern, etc.) per matched pattern but never attached it to `ClassifyResult`; codegen fell back to parsing `key:value` pairs out of the human-readable `traits` array, which never contained colons. As a result `src/worker.ts` registered zero routes for every scaffold, the `cron-worker` scheduled-handler wrapper and `[triggers]` wrangler stanza never fired, webhook codegen always emitted Stripe signature verification even for generic/GitHub-style webhooks, and D1 schema selection silently fell back to the generic `resources` table for every "worker"-bucketed pattern (Stripe webhook, generic webhook, hardening overlay, AI chat all share the coarse `PatternName` `'worker'`). `ClassifyResult` and `ScaffoldFacts` now carry `traitMap` and a `sourcePattern` field; all codegen/knowledge/wrangler lookups key off `sourcePattern` instead of the coarse `pattern` bucket. Also fixed: `inferBindings()` was being called with the coarse pattern name instead of the actual intention text, so D1/KV/R2/AI binding inference silently never ran (masked by a universal D1+KV fallback whenever no signal was detected). - **`@stackbilt/scaffold-core` project naming** (`tarotscript#427`) — `projectName` defaulted to the literal placeholder `'my-worker'`, and `wrangler.toml`/`package.json` had separate hardcoded names (`stackbilder-generated` / `stackbilder-scaffold`) that never matched each other or the contract filename. Added `deriveProjectSlug()` — derives a kebab-case slug (capped at 40 chars, falls back to `scaffold-app`) from the intention when no explicit `projectName` option is passed — and threaded the single resulting slug through wrangler.toml, package.json, the contract filename/description, and all `.ai/*.adf` headers. - **`@stackbilt/scaffold-core` empty/broken generated files** (`tarotscript#427`) — `.ai/adr-002.md` was always emitted, including as a 0-byte file when there were no compliance domains (`governance.adr002 ?? ''`); it is now omitted entirely when there's nothing to say. `.ai/core.adf` and `.ai/state.adf` rendered `### ` headings with blank bullet fields (no upstream rawFacts exist in the local scaffold-core flow); sections now render only when real content exists, with an honest fallback note otherwise. diff --git a/packages/scaffold-core/src/__tests__/output-quality.test.ts b/packages/scaffold-core/src/__tests__/output-quality.test.ts index 4ca84f5..55ab03d 100644 --- a/packages/scaffold-core/src/__tests__/output-quality.test.ts +++ b/packages/scaffold-core/src/__tests__/output-quality.test.ts @@ -224,3 +224,29 @@ describe('scaffold-core output quality — every generated import is a declared } }); }); + +describe('scaffold-core output quality — generated project typechecks clean', () => { + it.each(REPRESENTATIVE_INTENTIONS)('tsconfig pins lib to ES2022 (no lib.dom clash with workers-types) for: %s', (intention) => { + const result = buildScaffold(intention); + const tsconfig = JSON.parse(fileContent(result, 'tsconfig.json')); + expect(tsconfig.compilerOptions.lib).toEqual(['ES2022']); + expect(tsconfig.compilerOptions.skipLibCheck).toBe(true); + }); + + it.each(REPRESENTATIVE_INTENTIONS)('HttpError.status is a ContentfulStatusCode so onError c.json(err.status) typechecks for: %s', (intention) => { + const result = buildScaffold(intention); + const httpError = fileContent(result, 'src/lib/http-error.ts'); + if (!httpError) return; // pattern without the http-error helper + expect(httpError).toContain('ContentfulStatusCode'); + expect(httpError).not.toMatch(/status: number/); + }); + + it.each(REPRESENTATIVE_INTENTIONS)('json body catch fallbacks are typed to the request shape for: %s', (intention) => { + const result = buildScaffold(intention); + for (const f of result.files) { + // Bare `.catch(() => ({}))` widens the body union to {} and breaks + // property access under strict mode in the downloaded project. + expect(f.content).not.toContain('.catch(() => ({}))'); + } + }); +}); diff --git a/packages/scaffold-core/src/codegen/files.ts b/packages/scaffold-core/src/codegen/files.ts index 470d7e0..12ddde5 100644 --- a/packages/scaffold-core/src/codegen/files.ts +++ b/packages/scaffold-core/src/codegen/files.ts @@ -420,7 +420,11 @@ export function baseFiles(facts: ScaffoldFacts): ScaffoldFile[] { target: 'ES2022', module: 'ESNext', moduleResolution: 'Bundler', + // Without an explicit lib, tsc injects lib.dom, which conflicts + // with @cloudflare/workers-types globals (Response, caches, ...). + lib: ['ES2022'], strict: true, + skipLibCheck: true, types: ['@cloudflare/workers-types', 'vitest/globals'], }, include: ['src', 'tests'], @@ -505,10 +509,12 @@ export function baseFiles(facts: ScaffoldFacts): ScaffoldFile[] { path: 'src/lib/http-error.ts', role: 'entry', content: [ + 'import type { ContentfulStatusCode } from "hono/utils/http-status";', + '', 'export class HttpError extends Error {', - ' status: number;', + ' status: ContentfulStatusCode;', '', - ' constructor(status: number, message: string) {', + ' constructor(status: ContentfulStatusCode, message: string) {', ' super(message);', ' this.status = status;', ' }', diff --git a/packages/scaffold-core/src/codegen/routes.ts b/packages/scaffold-core/src/codegen/routes.ts index b9af14c..0044d16 100644 --- a/packages/scaffold-core/src/codegen/routes.ts +++ b/packages/scaffold-core/src/codegen/routes.ts @@ -160,7 +160,7 @@ export function routeContent( `export function ${fnName}(app: Hono<{ Bindings: Env }>) {`, ' app.post("/chat", async (c) => {', ' await requireBearerAuth(c);', - ' const body = await c.req.json<{ message?: string }>().catch(() => ({}));', + ' const body = await c.req.json<{ message?: string }>().catch(() => ({} as { message?: string }));', ' if (!body.message) throw new HttpError(400, "message is required");', '', ' const userContent = sanitizeUserContent(body.message);', @@ -228,7 +228,7 @@ export function routeContent( '', `export function ${fnName}(app: Hono<{ Bindings: Env }>) {`, ' app.post("/auth/login", async (c) => {', - ' const body = await c.req.json<{ email?: string }>().catch(() => ({}));', + ' const body = await c.req.json<{ email?: string }>().catch(() => ({} as { email?: string }));', ' if (!body.email) throw new HttpError(400, "email is required");', ' return c.json({ token: "replace-with-jwt", email: body.email });', ' });', @@ -271,7 +271,7 @@ export function routeContent( '', ' app.post("/resources", async (c) => {', ' await requireBearerAuth(c);', - ' const body = await c.req.json<{ name?: string }>().catch(() => ({}));', + ' const body = await c.req.json<{ name?: string }>().catch(() => ({} as { name?: string }));', ' if (!body.name || typeof body.name !== "string") throw new HttpError(400, "name is required");', ' const id = crypto.randomUUID();', ' await c.env.DB.prepare(', @@ -305,7 +305,7 @@ export function routeContent( ' app.put("/resources/:id", async (c) => {', ' await requireBearerAuth(c);', ' const id = c.req.param("id");', - ' const body = await c.req.json<{ name?: string }>().catch(() => ({}));', + ' const body = await c.req.json<{ name?: string }>().catch(() => ({} as { name?: string }));', ' if (!body.name) throw new HttpError(400, "name is required");', ' const result = await c.env.DB.prepare(', ' "UPDATE resources SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",',