Skip to content

fix(scaffold-core): derive project slug, add worker route stub, populate/omit empty templates, drop unpublished import#247

Merged
stackbilt-admin merged 3 commits into
mainfrom
fix/scaffold-core-output-quality
Jul 5, 2026
Merged

fix(scaffold-core): derive project slug, add worker route stub, populate/omit empty templates, drop unpublished import#247
stackbilt-admin merged 3 commits into
mainfrom
fix/scaffold-core-output-quality

Conversation

@stackbilt-admin

@stackbilt-admin stackbilt-admin commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

The stackbilder.com public hero demo (POST /api/scaffold/preview in stackbilt-web, calling buildScaffold from this package) currently ships broken output to every visitor and is the prime suspect for the stackbilt-web#151 conversion-funnel bounce regression. This mirrors the defects reported in tarotscript#427 (sibling report).

Root cause (defect #1 — zero routes in src/worker.ts): classify() computed a traitMap per matched pattern (route_shape, default_routes, source_pattern, etc.) but never attached it to ClassifyResult. Codegen's baseFiles()/buildRoutes() fell back to parsing key:value pairs out of the human-readable traits array (e.g. 'post-handler', 'hmac-stripe') — which never contains colons — so default_routes was always empty and src/worker.ts registered zero routes for every scaffold, regardless of pattern.

The same coarse-pattern/fine-source_pattern mismatch turned out to silently break several other things once I traced it:

  • The cron-worker scheduled-handler wrapper and wrangler [triggers] crons stanza never fired (checked against facts.pattern which is 'scheduled', never the literal 'cron-worker').
  • Webhook codegen always emitted Stripe signature verification even for generic/GitHub-style webhooks (the pattern === 'generic-webhook' branch check never matched).
  • D1 schema selection silently fell back to the generic resources table for every pattern sharing the coarse 'worker' PatternName bucket (Stripe webhook, generic webhook, hardening overlay, and AI-chat all collapse into 'worker').

Fix: ClassifyResult and ScaffoldFacts now carry traitMap: Record<string,string> and a derived sourcePattern: string field. All codegen/knowledge/wrangler lookups that need the fine-grained pattern key now use sourcePattern instead of the coarse pattern bucket. getKnowledge() (threat model / ADR / test plan content) is also called with sourcePattern now — previously it silently fell back to generic REST-API threats/ADR content for any non-workers-saas/mcp-server/rust-wasm pattern.

Defect #2 — project name chaos: wrangler.toml (name = "stackbilder-generated"), package.json (name: 'stackbilder-scaffold'), the contract filename/description (my-worker), and every .ai/*.adf header used three different hardcoded placeholders that never matched each other. Added deriveProjectSlug() — derives a kebab-case slug (lowercase, non-alphanumeric → -, collapsed, 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 all of the above.

Defect #3 — empty/broken templates: .ai/adr-002.md was always emitted, including as a 0-byte file when there were no compliance domains (governance.adr002 ?? ''). It's now omitted entirely when there's nothing to say. .ai/core.adf/.ai/state.adf rendered ### headings with blank bullet fields (the local scaffold-core flow has no upstream rawFacts to populate them from — that's a different, TarotScript-scaffold-cast-only pipeline). Sections now render only when real content exists, with an honest fallback note otherwise (e.g. "No product requirement detail available for this scaffold.").

Defect #4 — corrected mid-review, see below.

⚠️ Correction to defect #4 (post-review)

My first pass on defect #4 removed the @stackbilt/contracts import from the generated contract stub, on the premise (inherited from tarotscript#427) that the package was unpublished. That premise was stale — team-lead review verified against the live npm registry that @stackbilt/contracts is published at 0.8.0. I independently re-verified (npm view @stackbilt/contracts versions --json) before making any change.

The actual defect: the generated contract stub imports zod and @stackbilt/contracts, but the base package.json (generated by codegen/files.ts before the contract file exists) only ever declared hono. So a downloaded scaffold survived npm install and only failed later, at typecheck/build, with missing-module errors. Fix:

  • Reverted the contract stub back to defineContract(...) from @stackbilt/contracts (the original template was correct).
  • index.ts's post-processing step now backfills both zod and @stackbilt/contracts (bumped the stale ^0.2.1 pin in FIRST_PARTY_DEPS to ^0.8.0) onto package.json whenever a contract file is grafted in.
  • While re-verifying end-to-end against the real published package, found that ContractDefinition.surfaces is a required field in @stackbilt/contracts@0.8.0 (its two sub-fields, api/db, are each optional), but the template only emitted surfaces conditionally when an API/DB surface was detected. Every generated contract now always includes at least surfaces: {}.
  • Also checked (per team-lead request) whether scaffold-core injects the genuinely-unpublished @stackbilt/worker-observability (confirmed via npm view: 404s on the registry). It's referenced in FIRST_PARTY_DEPS but wasn't reachable through buildScaffold()'s actual output (only through the lower-level generateProjectFiles/materializeScaffold API, which is separately exported) — disabled its trigger with an explanatory comment as a defensive fix for that path too.
  • Replaced the two tests that encoded the wrong premise with: a class-invariant sweep (every module imported by any generated .ts file must be declared in the generated package.json, across 9 representative patterns — catches this whole defect family, not just today's instance), a positive assertion that the contract stub imports defineContract, and a regression guard against @stackbilt/worker-observability reappearing.

Verification

  • Materialized a generated scaffold to disk and ran npm install directly against the real npm registry: succeeds, @stackbilt/contracts@0.8.0 resolves. Confirmed via dynamic import() that the installed package actually exports defineContract before re-typechecking.
  • Re-ran npm run typecheck inside the generated project after the surfaces fix: zero contract/zod/@stackbilt/contracts-related errors remain.
  • Confirmed via a scoped git stash round-trip that the pre-existing @cloudflare/workers-types/TS-DOM-lib type conflicts and a couple of unrelated codegen type bugs (/auth/login route's .catch(() => ({})) typing, worker.ts's c.json(err, 500) overload) are pre-existing on main, not introduced by this change — noted as a known follow-up, out of scope here.
  • 99/99 scaffold-core tests pass (71 existing + 28 new regression tests in output-quality.test.ts).
  • Full monorepo: tsc --noEmit clean.

Scope note — one thing found and explicitly reverted

I initially also fixed inferBindings(classification.pattern, ...)inferBindings(classification.enrichedIntention, ...) in index.ts, since inferBindings's first parameter is documented/typed as free-form intention text but was being passed the coarse pattern name instead. This is a real bug (binding inference silently never ran on the actual intention; masked today by a universal D1+KV fallback whenever no signal is detected). I reverted it after end-to-end verification showed it changes which bindings get inferred (e.g. an R2-only file-upload intention) without changing what the hard-coded route codegen assumes exists (c.env.DB.prepare(...) in resources.ts regardless), producing scaffolds that fail tsc — the exact failure class this PR eliminates elsewhere. Left as a documented NOTE in index.ts for a follow-up that reconciles binding inference with each pattern's route-level DB/KV/R2 assumptions.

Consumer impact

  • stackbilt-web pins @stackbilt/scaffold-core at ^1.2.0 (installed 1.7.0); this PR bumps to 1.8.0 (minor — additive ClassifyResult/ScaffoldFacts fields, no shape changes to buildScaffold's consumed response fields: classification/traits/tier2_recommended/governance/files). Not published per policy (publish only after production testing) — after merge, publish, then bump stackbilt-web's dependency to pick this up before it reaches the live hero demo.
  • No changes to the buildScaffold() response shape that stackbilt-web's /api/scaffold/preview consumes — verified against the captured hero-demo response shape (classification, traits, tier2_recommended, governance, files); the new traitMap/sourcePattern fields live on classification/facts and are not part of that consumed subset.

Test plan

  • pnpm exec vitest run packages/scaffold-core — 99/99 pass
  • pnpm exec tsc --noEmit -p tsconfig.json — clean
  • Materialized scaffold + npm install + import() + npm run typecheck end-to-end verification against the real published @stackbilt/contracts (see above)
  • Post-merge: npm publish (per policy, only after production testing) + stackbilt-web dependency bump

🤖 Generated with Claude Code

…ate/omit empty templates, drop unpublished import

Root cause behind the zero-route worker.ts: classify() computed a traitMap
(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 contains colons, so default_routes
was always empty. The same coarse-pattern/fine-pattern mismatch also silently
broke: the cron-worker scheduled-handler wrapper and wrangler [triggers]
stanza, Stripe-vs-generic webhook signature dispatch, and D1 schema selection
(every pattern sharing the 'worker' bucket fell back to the generic
rest-api schema). ClassifyResult/ScaffoldFacts now carry traitMap and a
sourcePattern field; codegen/knowledge/wrangler lookups key off sourcePattern.

Also fixes project-name inconsistency (wrangler.toml/package.json/contract
filename previously used three different hardcoded placeholders), 0-byte
.ai/adr-002.md and blank-heading .ai/core.adf sections, and the generated
contract stub's import of the unpublished @stackbilt/contracts package
(swapped for a plain Zod-typed stub; zod is now declared as a dependency
whenever a contract file is emitted).

Verified end-to-end: materialized a generated scaffold and ran npm install
directly against it — succeeds, and no @stackbilt/contracts or missing-zod
module errors remain (both present on unmodified main).

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Comment thread packages/scaffold-core/src/index.ts Fixed
Kurt Overmier and others added 2 commits July 4, 2026 17:20
…s published

Team-lead review caught a stale premise: @stackbilt/contracts is published
(0.8.0 on npm), not unpublished as tarotscript#427 originally reported.
Reverts the previous commit's removal of the defineContract import and
instead fixes the actual defect — the generated package.json never declared
zod or @stackbilt/contracts even though the contract stub imports both, so
a downloaded scaffold survived `npm install` and only failed later at
typecheck/build. Both deps are now backfilled onto package.json whenever a
contract file is emitted, using the current 0.8.x version range.

Also found while re-verifying end-to-end against the real published package:
ContractDefinition.surfaces is a required field in @stackbilt/contracts@0.8.0,
but the template only emitted `surfaces` conditionally — every generated
contract now always includes at least `surfaces: {}`. And disabled the
@stackbilt/worker-observability injection in FIRST_PARTY_DEPS: that package
does genuinely 404 on the registry (verified via `npm view`), same defect
class as the corrected premise above, different package.

Replaced the two tests that asserted the wrong premise with: an
import-declaration class-invariant sweep (every module imported by any
generated .ts file must be declared in the generated package.json, across
9 representative patterns), a positive assertion that the contract stub
does import defineContract from @stackbilt/contracts, and a regression
guard against the disabled worker-observability injection reappearing.

Verified end-to-end: materialized a generated scaffold, ran `npm install`
against the real registry (succeeds, @stackbilt/contracts@0.8.0 resolves),
and confirmed `defineContract` really exists in the installed package's
exports before re-typechecking the generated project — no contract/zod/
stackbilt-related type errors remain.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Addresses a CodeQL finding (polynomial regex on uncontrolled data) on the
two trim regexes in deriveProjectSlug — they ran against the unbounded
intention string (public hero-demo input) before any length cap applied.
Truncate to a generous fixed length first; slugs are capped at 40 chars
regardless, so nothing past that prefix could ever change the output.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
@stackbilt-admin stackbilt-admin merged commit c66d96f into main Jul 5, 2026
5 checks passed
@stackbilt-admin stackbilt-admin deleted the fix/scaffold-core-output-quality branch July 5, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants