fix(scaffold-core): derive project slug, add worker route stub, populate/omit empty templates, drop unpublished import#247
Merged
Conversation
…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>
…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>
This was referenced Jul 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The stackbilder.com public hero demo (
POST /api/scaffold/previewin stackbilt-web, callingbuildScaffoldfrom 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 atraitMapper matched pattern (route_shape,default_routes,source_pattern, etc.) but never attached it toClassifyResult. Codegen'sbaseFiles()/buildRoutes()fell back to parsingkey:valuepairs out of the human-readabletraitsarray (e.g.'post-handler','hmac-stripe') — which never contains colons — sodefault_routeswas always empty andsrc/worker.tsregistered zero routes for every scaffold, regardless of pattern.The same coarse-
pattern/fine-source_patternmismatch turned out to silently break several other things once I traced it:cron-workerscheduled-handler wrapper and wrangler[triggers]crons stanza never fired (checked againstfacts.patternwhich is'scheduled', never the literal'cron-worker').pattern === 'generic-webhook'branch check never matched).resourcestable for every pattern sharing the coarse'worker'PatternNamebucket (Stripe webhook, generic webhook, hardening overlay, and AI-chat all collapse into'worker').Fix:
ClassifyResultandScaffoldFactsnow carrytraitMap: Record<string,string>and a derivedsourcePattern: stringfield. All codegen/knowledge/wrangler lookups that need the fine-grained pattern key now usesourcePatterninstead of the coarsepatternbucket.getKnowledge()(threat model / ADR / test plan content) is also called withsourcePatternnow — previously it silently fell back to generic REST-API threats/ADR content for any non-workers-saas/mcp-server/rust-wasmpattern.Defect #2 — project name chaos:
wrangler.toml(name = "stackbilder-generated"),package.json(name: 'stackbilder-scaffold'), the contract filename/description (my-worker), and every.ai/*.adfheader used three different hardcoded placeholders that never matched each other. AddedderiveProjectSlug()— derives a kebab-case slug (lowercase, non-alphanumeric →-, collapsed, capped at 40 chars, falls back toscaffold-app) from the intention when no explicitprojectNameoption is passed — and threaded the single resulting slug through all of the above.Defect #3 — empty/broken templates:
.ai/adr-002.mdwas 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.adfrendered###headings with blank bullet fields (the local scaffold-core flow has no upstreamrawFactsto 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.
My first pass on defect #4 removed the
@stackbilt/contractsimport 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/contractsis published at0.8.0. I independently re-verified (npm view @stackbilt/contracts versions --json) before making any change.The actual defect: the generated contract stub imports
zodand@stackbilt/contracts, but the basepackage.json(generated bycodegen/files.tsbefore the contract file exists) only ever declaredhono. So a downloaded scaffold survivednpm installand only failed later, at typecheck/build, with missing-module errors. Fix:defineContract(...)from@stackbilt/contracts(the original template was correct).index.ts's post-processing step now backfills bothzodand@stackbilt/contracts(bumped the stale^0.2.1pin inFIRST_PARTY_DEPSto^0.8.0) ontopackage.jsonwhenever a contract file is grafted in.ContractDefinition.surfacesis a required field in@stackbilt/contracts@0.8.0(its two sub-fields,api/db, are each optional), but the template only emittedsurfacesconditionally when an API/DB surface was detected. Every generated contract now always includes at leastsurfaces: {}.@stackbilt/worker-observability(confirmed vianpm view: 404s on the registry). It's referenced inFIRST_PARTY_DEPSbut wasn't reachable throughbuildScaffold()'s actual output (only through the lower-levelgenerateProjectFiles/materializeScaffoldAPI, which is separately exported) — disabled its trigger with an explanatory comment as a defensive fix for that path too..tsfile must be declared in the generatedpackage.json, across 9 representative patterns — catches this whole defect family, not just today's instance), a positive assertion that the contract stub importsdefineContract, and a regression guard against@stackbilt/worker-observabilityreappearing.Verification
npm installdirectly against the real npm registry: succeeds,@stackbilt/contracts@0.8.0resolves. Confirmed via dynamicimport()that the installed package actually exportsdefineContractbefore re-typechecking.npm run typecheckinside the generated project after thesurfacesfix: zero contract/zod/@stackbilt/contracts-related errors remain.git stashround-trip that the pre-existing@cloudflare/workers-types/TS-DOM-lib type conflicts and a couple of unrelated codegen type bugs (/auth/loginroute's.catch(() => ({}))typing,worker.ts'sc.json(err, 500)overload) are pre-existing on main, not introduced by this change — noted as a known follow-up, out of scope here.output-quality.test.ts).tsc --noEmitclean.Scope note — one thing found and explicitly reverted
I initially also fixed
inferBindings(classification.pattern, ...)→inferBindings(classification.enrichedIntention, ...)inindex.ts, sinceinferBindings'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(...)inresources.tsregardless), producing scaffolds that failtsc— the exact failure class this PR eliminates elsewhere. Left as a documentedNOTEinindex.tsfor a follow-up that reconciles binding inference with each pattern's route-level DB/KV/R2 assumptions.Consumer impact
stackbilt-webpins@stackbilt/scaffold-coreat^1.2.0(installed1.7.0); this PR bumps to1.8.0(minor — additiveClassifyResult/ScaffoldFactsfields, no shape changes tobuildScaffold'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.buildScaffold()response shape thatstackbilt-web's/api/scaffold/previewconsumes — verified against the captured hero-demo response shape (classification,traits,tier2_recommended,governance,files); the newtraitMap/sourcePatternfields live onclassification/factsand are not part of that consumed subset.Test plan
pnpm exec vitest run packages/scaffold-core— 99/99 passpnpm exec tsc --noEmit -p tsconfig.json— cleannpm install+import()+npm run typecheckend-to-end verification against the real published@stackbilt/contracts(see above)npm publish(per policy, only after production testing) + stackbilt-web dependency bump🤖 Generated with Claude Code