Skip to content

fix(showcase): reject unsubstituted {slug} placeholders at the deep-link source and hide the 2 live bad entries (#960) - #963

Merged
developer-ainative merged 2 commits into
mainfrom
bug/issue-960-showcase-placeholder-entries
Oct 6, 2026
Merged

developer-ainative merged 2 commits into
mainfrom
bug/issue-960-showcase-placeholder-entries

Conversation

@developer-ainative

Copy link
Copy Markdown
Contributor

Closes #960

Root cause — confirmed by exact reproduction, not inference

The issue correctly ruled out a server-side template-literal bug. Verified that independently: every ${slug} in app/api/build interpolates correctly, and both company-app/company-product prompt templates are well-formed.

The real origin is the deep-link entry point, contexts/build-context.tsx:

dispatch({ type: 'START_BUILD', idea: company, appSub: company, companyName: company })

It seeds idea, appSub and companyName from the raw ?company= query param. components/build/screens/Live.tsx then posts state.companyName as name and state.idea as idea to both /api/build/company-app and /api/build/company-product, each of which embeds both values verbatim in its real codegen prompt.

So anything in ?company= becomes a real, billable generation and a public showcase entry. Nothing between the URL bar and the LLM prompt ever questioned the value.

Someone opened /build?screen=live&company={slug} with the markdown code-span's closing backtick still attached — a URL copied verbatim out of this repo's own documentation. docs/growth/WINBACK_EMAIL_2026-08-27.md line 23 literally contained:

`?screen=live&company={slug}` (durable)

components/build/StandalonePreviewRegenerate.tsx's own doc comment carries the same placeholder in backticks. That trailing backtick is the whole tell, and it's why both the title AND the company name were identical.

The reproduction

Fed {slug} + backtick as the single input through the real, unmodified functions:

Live production row Reproduced
company-app title {slug}` {slug}`
company-app slug slug-x6wOHR slug-x6wOHR
company-product title {slug}` {slug}`
company-product slug slug-7W6siL slug-7W6siL

Both prompt prefixes match the live rows character for character.

The slugs are derived, not stored: app/api/showcase/route.ts builds them as generateSlug(title) + '-' + chat_id.slice(0, 6). generateSlug("{slug}")→slug, and the mixed-case suffix is just the first 6 chars of the nanoid chatId — which is why x6wOHR/7W6siL` looked like a random suffix no local script generates. The real rows:

  • chat_id x6wOHR9rN8UDxcUyTXElZ → slug-x6wOHR (company-app / landing page)
  • chat_id 7W6siLoHGM8O4rsusuIQt → slug-7W6siL (company-product / real app)

Both dated 2026-09-23. Confirmed no builder_app_registry row exists for either (registerApp never ran) — consistent with a one-off URL open that never completed the flow, not a repeating caller.

Ruled out: no committed script, seed, test, agent/skill surface, or MCP tool POSTs these routes (tests are source-inspection only); nothing in git history around 2026-09-23 touched them; StandalonePreviewRegenerate's idea/name come from the app-registry so it can't invent the string.

The fix — three layers, all existing mechanisms

1. Prevention at the real source (contexts/build-context.tsx) — a ?company= carrying {...}/${...} or a stray backtick no longer starts a build. Handled exactly like the existing confirmed-not-found case (deepLinkNotFound + companies screen) rather than silently generating from documentation text. New pure exported isUsableDeepLinkCompany, testable without mounting BuildProvider (which OOMs jsdom via useAutoplay).

2. API-boundary backstop (company-app, company-product) — both routes now 400 on an idea/name containing unsubstituted template syntax, before touching the registry or kicking off any generation. Covers every other caller, present and future. This is the validation the issue suggested.

3. Removal of the 2 live entries (app/api/showcase/route.ts) — a read-time filter on the derived title, sitting directly next to the existing is_showcase gate.

Why read-time and not is_showcase: both rows were persisted with is_showcase: true and pass isQualityApp, because nothing was wrong with their code — only with the name/idea the generation was asked for. Neither pre-existing gate could ever have excluded them, and is_showcase is computed at persist time so it can't retroactively fix a stored row. ZeroDB rows here are append-only/latest-wins with no delete path, so read-time exclusion is this codebase's established way to keep a bad historical row off the public gallery — the same place and pattern the is_showcase filter already uses. No new mechanism invented.

Checks the title only, deliberately not the whole prompt: a real founder's free-text idea could legitimately contain a backtick while describing code, and silently hiding a real founder's app would be a worse failure than the cosmetic one being fixed.

The guard lives in its own import-free lib/build/placeholder-guard.ts so the client bundle doesn't pull SEED_SHOWCASE in for a regex; showcase-data.ts re-exports it so both paths share one definition.

Tests

50 new tests across 4 files, built from the actual live rows (verbatim prompts and chat_ids fetched from production):

  • __tests__/lib/placeholder-guard-960.test.ts (22)
  • __tests__/contexts/deep-link-company-placeholder-960.test.ts (10)
  • __tests__/api/showcase-placeholder-entries-960.test.ts (6)
  • __tests__/api/company-routes-placeholder-guard-960.test.ts (12)

Deliberately weighted toward the false-positive direction, since rejecting a real founder's build would be strictly worse than the bug: 20+ realistic company names and founder ideas (O'Brien & Sons, Café Müller, C++ tutoring platform, cost: $19, 100% Organic, Acme (Holdings) Ltd., Twenty-Four/Seven) must all still pass. The API tests also assert the guard rejects before the registry or generation is touched, and that the pre-existing idea and slug required behavior is unchanged.

Verification

npx vitest run     -> 6510 passed | 1 failed | 50 skipped (6561)
npx tsc --noEmit   -> clean

The single failure is pre-existing and unrelated: __tests__/lib/build/task-splitter.test.ts ("never throws when splitTaskViaLLM itself throws past its own internal catch", #904) times out at 5000ms. Confirmed it fails identically on clean origin/main with these changes stashed. Not touched, per the surgical-changes rule — flagging it here rather than fixing it in this PR.

Confidence

High on the mechanism (?company= → companyName/idea → both codegen prompts): byte-for-byte reproduction of both titles, both derived slugs, and both prompts from one input, through unmodified production code.

High on the source of the string (a placeholder URL copied out of this repo's own markdown): the trailing backtick is a code-span delimiter with no other plausible origin, and the exact placeholder URL exists in two files here.

Not determinable — and not claimed: who opened it and when exactly. There is no request log or audit row for an anonymous deep-link page load, and the registry rows that would have carried an owner were never written. That gap doesn't affect the fix: all three layers are keyed on the shape of the input, not on who sent it.

Follow-up (not in this PR)

The 2 bad rows still physically exist in the generations table; they are now excluded at read time, which is the established pattern, but there is no delete/tombstone path for a ZeroDB generation row. Worth a separate issue if a real hard-delete mechanism is wanted.

…ink source and hide the 2 live bad entries

Refs #960

Root cause, confirmed by exact byte-for-byte reproduction (not inference):
contexts/build-context.tsx's deep-link effect seeds idea, appSub AND
companyName from the RAW ?company= query param:

  dispatch({ type: 'START_BUILD', idea: company, appSub: company, companyName: company })

Live.tsx then posts state.companyName as `name` and state.idea as `idea` to
BOTH /api/build/company-app and /api/build/company-product, each of which
embeds both values verbatim in its real codegen prompt. So anything in
?company= becomes a real, billable generation and a public showcase entry.

Someone opened /build?screen=live&company={slug} with the markdown code-span's
closing backtick still attached — a URL copied verbatim out of this repo's own
docs (docs/growth/WINBACK_EMAIL_2026-08-27.md line 23 literally contained
`?screen=live&company={slug}` inside backticks). That produced the two live
entries, one per template, both titled literally {slug} + a backtick:

  chat_id x6wOHR9rN8UDxcUyTXElZ -> slug "slug-x6wOHR" (company-app)
  chat_id 7W6siLoHGM8O4rsusuIQt -> slug "slug-7W6siL" (company-product)

Reproduced from that single input through the real, unmodified functions:
extractShowcaseTitle returns "{slug}`" (its #955 quotedNameMatch branch), and
app/api/showcase/route.ts's generateSlug(title) + chat_id.slice(0,6) yields
exactly slug-x6wOHR and slug-7W6siL. Both prompt prefixes match the live rows
character for character. This is NOT a server-side template-literal bug —
every ${slug} in app/api/build interpolates correctly, as the issue suspected.

Fix, three layers, all using mechanisms already established here:

1. Prevention at the real source (contexts/build-context.tsx): a ?company=
   carrying {...}/${...} or a stray backtick no longer starts a build. Handled
   exactly like the existing confirmed-not-found case (deepLinkNotFound +
   companies screen) rather than silently generating from documentation text.

2. API-boundary backstop (company-app, company-product): both routes now 400
   on an idea/name containing unsubstituted template syntax, before touching
   the registry or kicking off any generation — covering every other caller.

3. Removal of the 2 live entries (app/api/showcase/route.ts): a read-time
   filter on the derived title, sitting next to the existing is_showcase gate.
   Both rows were persisted with is_showcase: true and pass isQualityApp —
   nothing was wrong with their CODE, only with the name they were generated
   for — so neither pre-existing gate could ever exclude them. ZeroDB rows
   here are append-only/latest-wins with no delete path, so read-time
   exclusion is this codebase's established way to keep a bad historical row
   off the public gallery. Checks the title only, not the whole prompt: a real
   founder's free-text idea could legitimately contain a backtick, and hiding
   a real app would be worse than the cosmetic bug being fixed.

The guard lives in its own import-free lib/build/placeholder-guard.ts so the
client bundle doesn't pull SEED_SHOWCASE in for a regex; showcase-data.ts
re-exports it so both paths share one definition.

Tests: 50 new across 4 files, built from the ACTUAL live rows (verbatim
prompts/chat_ids fetched from production). Covers both the true-positive
direction and, more importantly, the false-positive direction — 20+ realistic
company names and founder ideas (O'Brien & Sons, Café Müller, C++ tutoring,
"cost: $19", 100% Organic) must all still pass, since rejecting a real build
would be strictly worse than the bug.

Verified: npx vitest run -> 6510 passed, 1 pre-existing unrelated failure
(__tests__/lib/build/task-splitter.test.ts #904 LLM-throw timeout, confirmed
failing identically on clean origin/main with these changes stashed — not
touched, per surgical-changes). npx tsc --noEmit clean.
…e-placeholder-entries

# Conflicts:
#	contexts/build-context.tsx
@developer-ainative
developer-ainative merged commit 81a4803 into main Oct 6, 2026
1 check passed
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.

bug(showcase): two live entries show literal unresolved '{slug}' template placeholder as title/company name

1 participant