Recover a built-in catalog clobbered by the empty scaffold¶
Last reviewed: 2026-08-31
Use this when a built-in A2UI catalog (financial-services, standard-primitives,
common-extended, retail, utilities, healthcare, insurance) has lost its real
components — its surfaces render blank or show only the starter ExampleCard — after
a seed-builtins run published the empty scaffold over it. This is the 2026-08-27
incident described in
catalog-builder.md → scaffold-clobber guard.
The publish-layer guard now prevents
the clobber from recurring, but a catalog already pointing at a scaffold 2.0.0 (or whose
real bundle was never uploaded to the target bucket) needs an explicit re-publish from the
baked real source.
The "blank / empty-bucket" case below is now auto-healed. As of 2026-08-31 catalog-builder runs an idempotent built-in self-heal on startup (and via
POST /seed-builtins) that re-publishes any built-in whose current bundle is missing from this env's bucket. So the second row of the table (good pointer, bundle never uploaded) reconciles on the next deploy or pod restart with no manual step. This runbook is still the recovery for the first row (a scaffold2.0.0is the current version): the self-heal sees areadybundle that does exist and leaves it alone, so a deliberateallowScaffoldResetre-publish from real source is still required.Cloned (non-TrueServe) tenants are also auto-healed as of 2026-09-01. A tenant provisioned before the shared→tenant rescope inherited the source's pre-rescope
bundle_urlverbatim (catalogs/shared/<slug>/…) and 404s even after the source tenant is healed — the source-only self-heal never touched it.reconcileClonedBuiltins(the self-heal's second pass) re-points such a clone onto the source's healed bundle on the next deploy or pod restart — no manual step. It only touches clones whose own bundle object is missing, so a tenant's own edits are never overwritten.
Two distinct failure states¶
Both are fixed by the same action (re-publish from real source), but they look different:
| Symptom | DB state | Cause |
|---|---|---|
Preview renders, but shows only ExampleCard |
current_version_id → a 2.0.0 whose source_manifest is the scaffold; its bundle is in the bucket |
The seed publish succeeded against a stale scaffold and deleted every real component (financial-services, standard-primitives). |
| Preview blank (manifest URL 404s) | current_version_id → a real-component version, but that bundle was never uploaded to this env's bucket |
The seed publish failed (swallowed) — the good pointer survived but the bundle isn't in the bucket. The original empty-bucket bug. |
A bare current_version_id repoint does not fix either: the scaffold 2.0.0 bundle is
wrong, and the good older bundles were never uploaded to this env's bucket. You must
re-publish so a real bundle is (re)built and uploaded.
Why the real source is safe¶
The real component source is baked into the catalog-builder image at
/app/catalogs/shared/<slug> and was never touched. Only the PVC workspace copy
(/workspaces/catalog-tenant-trueserve-<slug>) held the empty scaffold. Confirm before
recovering:
# In a catalog-builder pod:
ls /app/catalogs/shared/financial-services/components # real: AccountOverview.tsx, …
ls /workspaces/catalog-tenant-trueserve-financial-services/components # stale: only ExampleCard.tsx
Recovery¶
Run against one environment at a time; verify the connected DB/bucket before starting. All steps are ArgoCD-safe (no chart change is required — the fix is a data/PVC repair).
1. Restore real source onto the PVC¶
If the workspace components/ holds only ExampleCard.tsx, replace the directory with the
baked source. From inside a catalog-builder pod (the app container that mounts the PVC):
for slug in standard-primitives financial-services; do # add any other clobbered slugs
t="/workspaces/catalog-tenant-trueserve-$slug"
rm -rf "$t" && cp -a "/app/catalogs/shared/$slug" "$t"
chown -R 1000:1000 "$t" 2>/dev/null || true
# Images built before the 2026-08-28 fix bake an inert `tailwind.config.ts`
# into each built-in source. Tailwind v4 catalogs are configured entirely in
# styles.css (@source/@theme), and `assertSafeTailwindInputs` HARD-REJECTS a
# JS/TS config — so leaving it in place makes step 2's publish fail with
# `HTTP 500: JavaScript Tailwind config files are not allowed`. Delete it.
rm -f "$t/tailwind.config.ts"
done
On a later chart rollout the source seeder now self-heals this same condition (
values.yaml) — but do the manual restore now rather than wait for a deploy. Note the seeder re-copies the baked source verbatim, so on a pre-fix image it re-addstailwind.config.tson every pod restart; the permanent fix is to drop the file from the built-in sources in the image (done 2026-08-28).
2. Re-publish each catalog from the restored source¶
Publish by slug against the TrueServe tenant, version:"auto" — the auto path bumps past
the taken 2.0.0 and uploads a fresh bundle. Because the source is now the real
components (not the scaffold), the publish-layer guard does not fire:
TOKEN="$CATALOG_BUILDER_SERVICE_TOKEN" # from trueserve-catalog-builder-secrets
for slug in standard-primitives financial-services; do
curl -fsS -X POST "http://catalog-builder:8090/publish?bySlug=true" \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d "{\"slug\":\"$slug\",\"scope\":\"tenant\",\"tenantId\":\"00000000-0000-0000-0000-000000000001\",\"version\":\"auto\"}"
done
Order matters: publish standard-primitives first — the other built-ins render their
components against its primitives.
3. Verify¶
# current version now carries real components again (not just ExampleCard):
psql "$DATABASE_URL" -c "
SELECT cat.slug, v.version, v.bump_kind,
(SELECT string_agg(c->>'name', ',')
FROM jsonb_array_elements(v.source_manifest->'components') c) AS components
FROM a2ui_catalogs cat
JOIN a2ui_catalog_versions v ON v.id = cat.current_version_id
WHERE cat.slug IN ('standard-primitives','financial-services');"
Then open a surface that uses each catalog and confirm the real components render.
Don't forget qa / prod¶
The same stale scaffold likely sits on the qa and prod PVCs. Their built-ins were spared
only because their seed publishes happened to fail — a clean re-seed there would clobber
them. Before any catalog re-seed on qa/prod, either deploy the seeder-heal + publish guard
(this change) or run step 1 there first. Confirm each env's PVC with the ls check above.
See also¶
- catalog-builder.md — seeding, scope, and the scaffold-clobber guard.