@mohasinac/appkit 4.45.0 → 4.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/categories/live-counts.d.ts +41 -0
  3. package/dist/_internal/server/features/categories/live-counts.js +72 -0
  4. package/dist/_internal/server/features/checkout/expand-order-items.js +17 -0
  5. package/dist/_internal/server/features/orders/adapters.js +29 -0
  6. package/dist/_internal/server/features/parts/assembly.d.ts +126 -0
  7. package/dist/_internal/server/features/parts/assembly.js +246 -0
  8. package/dist/_internal/server/features/parts/data.d.ts +65 -0
  9. package/dist/_internal/server/features/parts/data.js +170 -0
  10. package/dist/_internal/server/features/parts/og.d.ts +34 -0
  11. package/dist/_internal/server/features/parts/og.js +66 -0
  12. package/dist/_internal/server/features/search/search-everything.d.ts +75 -0
  13. package/dist/_internal/server/features/search/search-everything.js +120 -0
  14. package/dist/_internal/server/features/search/search-sources.d.ts +17 -0
  15. package/dist/_internal/server/features/search/search-sources.js +138 -0
  16. package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.d.ts +70 -0
  17. package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +302 -0
  18. package/dist/_internal/server/jobs/core/countersReconcile.js +128 -19
  19. package/dist/_internal/server/jobs/core/jobRunners.js +12 -0
  20. package/dist/_internal/server/jobs/core/onCategoryWrite.js +73 -3
  21. package/dist/_internal/server/jobs/core/onProductWrite.js +93 -43
  22. package/dist/_internal/server/jobs/core/productViewWindows.d.ts +2 -0
  23. package/dist/_internal/server/jobs/core/productViewWindows.js +173 -0
  24. package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
  25. package/dist/_internal/server/jobs/core/testDataCleanup.js +27 -1
  26. package/dist/_internal/server/jobs/handlers/pageViewRollup.d.ts +20 -0
  27. package/dist/_internal/server/jobs/handlers/pageViewRollup.js +25 -1
  28. package/dist/_internal/shared/actions/action-registry.js +36 -0
  29. package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
  30. package/dist/_internal/shared/features/shipping/protections.d.ts +93 -1
  31. package/dist/_internal/shared/features/shipping/protections.js +127 -0
  32. package/dist/_internal/shared/search/part-aliases.d.ts +44 -0
  33. package/dist/_internal/shared/search/part-aliases.js +104 -0
  34. package/dist/constants/api-endpoints.d.ts +18 -12
  35. package/dist/constants/api-endpoints.js +6 -8
  36. package/dist/constants/field-names.d.ts +0 -2
  37. package/dist/constants/field-names.js +0 -2
  38. package/dist/constants/index.d.ts +1 -1
  39. package/dist/constants/index.js +1 -1
  40. package/dist/core/index.d.ts +0 -3
  41. package/dist/core/index.js +0 -2
  42. package/dist/errors/messages.d.ts +0 -7
  43. package/dist/errors/messages.js +0 -8
  44. package/dist/features/admin/components/AdminSectionsView.js +83 -32
  45. package/dist/features/admin/components/AdminSiteSettingsView.js +6 -2
  46. package/dist/features/admin/components/sections/sectionTemplates.d.ts +117 -0
  47. package/dist/features/admin/components/sections/sectionTemplates.js +213 -0
  48. package/dist/features/admin/schemas/firestore.d.ts +15 -0
  49. package/dist/features/admin/types/index.d.ts +0 -1
  50. package/dist/features/auth/permissions/constants.d.ts +1 -1
  51. package/dist/features/auth/permissions/constants.js +0 -2
  52. package/dist/features/categories/api/[id]/description-template/route.js +40 -1
  53. package/dist/features/categories/components/BrandDetailPageView.js +14 -3
  54. package/dist/features/categories/components/CategoryDetailPageView.js +89 -14
  55. package/dist/features/categories/components/CategoryTemplatePicker.js +9 -1
  56. package/dist/features/categories/repository/categories.repository.d.ts +65 -0
  57. package/dist/features/categories/repository/categories.repository.js +168 -22
  58. package/dist/features/categories/repository/category-counts.d.ts +143 -0
  59. package/dist/features/categories/repository/category-counts.js +224 -0
  60. package/dist/features/categories/schemas/category-counts.d.ts +155 -0
  61. package/dist/features/categories/schemas/category-counts.js +116 -0
  62. package/dist/features/categories/schemas/firestore.d.ts +0 -2
  63. package/dist/features/categories/schemas/firestore.js +0 -1
  64. package/dist/features/categories/utils/breadcrumb-chain.d.ts +194 -0
  65. package/dist/features/categories/utils/breadcrumb-chain.js +202 -0
  66. package/dist/features/corporate/schemas/index.d.ts +4 -4
  67. package/dist/features/orders/schemas/firestore.d.ts +13 -0
  68. package/dist/features/orders/types/index.d.ts +30 -0
  69. package/dist/features/parts/components/AssemblyDrawer.d.ts +107 -0
  70. package/dist/features/parts/components/AssemblyDrawer.js +250 -0
  71. package/dist/features/parts/components/ModelCompositionPanel.d.ts +37 -0
  72. package/dist/features/parts/components/ModelCompositionPanel.js +34 -0
  73. package/dist/features/parts/components/PartDetailPageView.d.ts +48 -0
  74. package/dist/features/parts/components/PartDetailPageView.js +20 -0
  75. package/dist/features/parts/components/PartsIndexPageView.d.ts +33 -0
  76. package/dist/features/parts/components/PartsIndexPageView.js +17 -0
  77. package/dist/features/parts/schemas/firestore.d.ts +257 -0
  78. package/dist/features/parts/schemas/firestore.js +181 -0
  79. package/dist/features/parts/utils/colourway-match.d.ts +62 -0
  80. package/dist/features/parts/utils/colourway-match.js +98 -0
  81. package/dist/features/parts/utils/part-breakdown.d.ts +53 -0
  82. package/dist/features/parts/utils/part-breakdown.js +77 -0
  83. package/dist/features/products/components/ProductDetailPageView.d.ts +14 -1
  84. package/dist/features/products/components/ProductDetailPageView.js +22 -3
  85. package/dist/features/products/components/ProductTabsShell.d.ts +3 -1
  86. package/dist/features/products/components/ProductTabsShell.js +4 -1
  87. package/dist/features/products/repository/category-assignment.d.ts +132 -0
  88. package/dist/features/products/repository/category-assignment.js +304 -0
  89. package/dist/features/products/repository/products.repository.d.ts +58 -1
  90. package/dist/features/products/repository/products.repository.js +255 -28
  91. package/dist/features/products/schemas/firestore.d.ts +147 -6
  92. package/dist/features/products/schemas/firestore.js +49 -0
  93. package/dist/features/search/components/SearchResultsPageView.d.ts +33 -0
  94. package/dist/features/search/components/SearchResultsPageView.js +21 -0
  95. package/dist/features/seller/components/SellerProductsView.js +129 -3
  96. package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -15
  97. package/dist/features/tester/seed-data/authored/content-discovery__parts-database.d.ts +2 -0
  98. package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +166 -0
  99. package/dist/features/tester/seed-data/authored/index.js +2 -0
  100. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +28 -1
  101. package/dist/index.d.ts +11 -16
  102. package/dist/index.js +26 -18
  103. package/dist/next/routing/route-map.d.ts +6 -2
  104. package/dist/next/routing/route-map.js +14 -1
  105. package/dist/providers/db-firebase/base.repository.d.ts +31 -0
  106. package/dist/providers/db-firebase/base.repository.js +31 -0
  107. package/dist/repositories/index.d.ts +1 -2
  108. package/dist/repositories/index.js +0 -1
  109. package/dist/seed/_helpers/category-content.d.ts +29 -0
  110. package/dist/seed/_helpers/category-content.js +98 -1
  111. package/dist/seed/_helpers/category-forest.js +104 -24
  112. package/dist/seed/_helpers/category-parts.d.ts +9 -0
  113. package/dist/seed/_helpers/category-parts.js +4594 -0
  114. package/dist/seed/carousel-slides-seed-data.js +81 -8
  115. package/dist/seed/categories-seed-data.js +44 -6
  116. package/dist/seed/homepage-sections-seed-data.js +170 -9
  117. package/dist/seed/index.d.ts +2 -0
  118. package/dist/seed/index.js +13 -0
  119. package/dist/seed/model-composition-seed-data.d.ts +15 -0
  120. package/dist/seed/model-composition-seed-data.js +8918 -0
  121. package/dist/seed/part-glossary-seed-data.d.ts +15 -0
  122. package/dist/seed/part-glossary-seed-data.js +10729 -0
  123. package/dist/seed/products-standard-seed-data.js +52 -0
  124. package/dist/seed/seed-purpose.d.ts +103 -4
  125. package/dist/seed/seed-purpose.js +162 -11
  126. package/dist/seo/json-ld.d.ts +10 -1
  127. package/dist/seo/json-ld.js +9 -1
  128. package/dist/server.d.ts +7 -0
  129. package/dist/server.js +11 -0
  130. package/dist/styles.css +1 -1
  131. package/dist/tailwind-utilities.css +1 -1
  132. package/dist/utils/id-generators.d.ts +30 -0
  133. package/dist/utils/id-generators.js +31 -1
  134. package/dist/utils/search-txt-builders.js +22 -0
  135. package/dist/utils/search-txt.d.ts +22 -0
  136. package/dist/utils/search-txt.js +17 -0
  137. package/dist/values/success-messages.d.ts +0 -4
  138. package/dist/values/success-messages.js +0 -5
  139. package/package.json +1 -1
  140. package/scripts/seed-cli.mjs +292 -16
  141. package/dist/core/copilot-log.repository.d.ts +0 -44
  142. package/dist/core/copilot-log.repository.js +0 -61
  143. package/dist/features/copilot/components/AdminCopilotView.d.ts +0 -21
  144. package/dist/features/copilot/components/AdminCopilotView.js +0 -51
  145. package/dist/features/copilot/components/index.d.ts +0 -2
  146. package/dist/features/copilot/components/index.js +0 -1
  147. package/dist/features/copilot/hooks/useCopilotChat.d.ts +0 -25
  148. package/dist/features/copilot/hooks/useCopilotChat.js +0 -65
  149. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +0 -15
  150. package/dist/features/copilot/hooks/useCopilotFeedback.js +0 -15
  151. package/dist/features/copilot/index.d.ts +0 -3
  152. package/dist/features/copilot/index.js +0 -3
@@ -194,8 +194,46 @@ const _rawSlides = [
194
194
  updatedAt: daysAgo(3),
195
195
  },
196
196
  {
197
+ /*
198
+ * ── G1c — THE AUTHENTICITY SLIDE ────────────────────────────────────────
199
+ *
200
+ * 🛑 This slide's id has said `authenticated-originals` since it was
201
+ * written and its copy said "Turn your collection into income" — a
202
+ * seller-acquisition message under an authenticity name. So authenticity,
203
+ * which is this marketplace's actual differentiator, had NO hero surface
204
+ * at all while the id implied it did.
205
+ *
206
+ * It is the one hero message the crawl independently validates three ways:
207
+ * beybladeartshop runs it as their slide 5 ("100% original Takara Tomy
208
+ * Beyblades. Don't fall for cheap imitations!"); worldhobbyshop — the SEO
209
+ * leader of the fifteen — ships a top-level "MidFake Beyblades" category
210
+ * and an FAQ promising that midfake and reproduction stock is kept
211
+ * separate with authenticity disclosed; and we already have the feature
212
+ * (`ProductAuthenticity` is a REQUIRED field with a badge and four FAQ
213
+ * rows) with nowhere on the homepage that says so.
214
+ *
215
+ * 🛑 THE COPY ANSWERS "AFTERMARKET", NOT "FAKE", and that is the finding
216
+ * that matters. The counterfeit seller measured in section 3.7 makes **zero**
217
+ * authenticity claims across 1,277 listings — no "original", no "replica",
218
+ * no "fake" — and sells itself as "a popular brand" offering "custom parts
219
+ * or upgraded components", "like many other aftermarket brands". So
220
+ * "aftermarket" and "custom" are the words our buyers will actually be
221
+ * sold, and a slide that only rebuts the word "fake" rebuts nothing they
222
+ * will hear.
223
+ *
224
+ * It also states the three-tier vocabulary rather than a binary, because
225
+ * the tiers are the honest part: a reproduction IS sellable when labelled,
226
+ * and claiming everything is original would be the same unfalsifiable
227
+ * promise the counterfeiter makes in reverse.
228
+ *
229
+ * 🛑 Seller acquisition is NOT lost — it keeps its own `banner` section
230
+ * ("Have collectibles to sell?" → "Start selling →"), the `features` and
231
+ * `stats` sections, and two nav entries. Verified before this was changed.
232
+ * `MAX_ACTIVE_SLIDES` is 5 and all five were taken, so this is a swap
233
+ * rather than an addition.
234
+ */
197
235
  id: "slide-authenticated-originals",
198
- title: "Sell on the marketplace",
236
+ title: "Original, reproduction, or unverified — every listing says which",
199
237
  order: 5,
200
238
  active: true,
201
239
  background: {
@@ -205,18 +243,53 @@ const _rawSlides = [
205
243
  },
206
244
  overlay: {
207
245
  enabled: true,
208
- subtitle: "For sellers",
209
- title: "Turn your collection into income",
210
- description: "Open a store in minutes, list for free, and get paid on a predictable schedule.",
246
+ subtitle: "Authenticity, stated",
247
+ // 🛑 "We make sellers say which" was the same overclaim as the body —
248
+ // the field is optional, so nobody is MADE to. What is true is that
249
+ // every listing carries a label, which is the verifiable version.
250
+ title: "“Aftermarket” is not a brand. Every listing says which.",
251
+ /*
252
+ * 🛑 THE CLAIM WAS FALSE WHEN FIRST WRITTEN, and the check is the point.
253
+ *
254
+ * It read "the seller has to pick one before it can go live" — and
255
+ * `authenticity` is `.optional()` in the write schema while being ABSENT
256
+ * on all 71 live products. A hero promise nobody could keep, on the one
257
+ * subject where this marketplace's credibility rests.
258
+ *
259
+ * Both halves were fixed before the copy was: the root default now makes
260
+ * 381/381 listing nodes resolve a value, and `withDerivedCategoryFields`
261
+ * stamps it on every seeded product (measured after loading:
262
+ * `original=4`, `unverified=67`, zero absent).
263
+ *
264
+ * The copy still says what is TRUE rather than what would be strongest.
265
+ * A seller may still omit the field — the category then decides, and it
266
+ * decides `unverified`, never `original`. So the promise is "every
267
+ * listing carries a label and we never silently claim original", which
268
+ * is both accurate and the more useful thing to say.
269
+ */
270
+ description: "Every listing carries one of three labels — original, reproduction or unverified. A seller can say which; if they do not, it reads “unverified”, because we will not claim “original” on your behalf. Reproductions are welcome here when they are labelled; a listing that leaves you guessing is not.",
211
271
  button: {
212
- id: "slide-sell-cta",
213
- text: "Start selling",
214
- link: String(ROUTES.PUBLIC.SELLER_GUIDE),
272
+ id: "slide-authenticity-cta",
273
+ text: "How to spot a fake",
274
+ /*
275
+ * 🛑 `/faqs/product_information`, the CATEGORY — not a question slug.
276
+ * The route is `/faqs/[category]` and it matches on
277
+ * `CATEGORY_KEYS.includes(category)`, so `/faqs/faq-how-to-spot-a-fake`
278
+ * would silently fall back to "all" and land the reader on the full
279
+ * index instead of the answer. Verified against the route before
280
+ * writing it, because the FAQ documents DO carry a `seo.slug` of
281
+ * exactly that shape, which makes the wrong link the obvious one.
282
+ *
283
+ * This category holds both authenticity answers — "How do I spot a fake
284
+ * Beyblade?" and "What is a reproduction or midfake? Is it a fake?" —
285
+ * which is the pair the slide's claim rests on.
286
+ */
287
+ link: "/faqs/product_information",
215
288
  variant: "primary",
216
289
  openInNewTab: false,
217
290
  },
218
291
  },
219
- link: { url: String(ROUTES.PUBLIC.SELLER_GUIDE), openInNewTab: false },
292
+ link: { url: "/faqs/product_information", openInNewTab: false },
220
293
  settings: SLIDE_SETTINGS,
221
294
  createdBy: "user-admin-letitrip",
222
295
  createdAt: daysAgo(40),
@@ -57,6 +57,7 @@ import { withCategorySearchTxt } from "./_helpers/search-txt-wrappers.js";
57
57
  import { CATEGORY_FOREST } from "./_helpers/category-forest.js";
58
58
  import { BRAND_ROW_COUNT, buildBrandRows } from "./_helpers/brand-rows.js";
59
59
  import { MODEL_LEAF_COUNT } from "./_helpers/category-models.js";
60
+ import { PART_LEAF_COUNT } from "./_helpers/category-parts.js";
60
61
  import { MARKET_BAND_LEAF_IDS } from "./_helpers/category-content.js";
61
62
  import { productsStandardSeedData } from "./products-standard-seed-data.js";
62
63
  const NOW = new Date();
@@ -461,12 +462,49 @@ const allCategoryRows = [
461
462
  * (the seed CLI, the tester fixtures, the audits) wants to fail before writing
462
463
  * rather than after.
463
464
  */
464
- const modelLeafRows = rawCategories.filter((c) => c.tier === 4).length;
465
- if (modelLeafRows !== MODEL_LEAF_COUNT) {
466
- throw new Error(`category seed: ${modelLeafRows} tier-4 model leaves reached the tree but ` +
467
- `category-models.ts generated ${MODEL_LEAF_COUNT}. A line id in the ` +
468
- `forest no longer matches a key in MODEL_LEAVES — re-run ` +
469
- `node scripts/gen-category-models.mjs and check the line ids.`);
465
+ /*
466
+ * 🛑 TIER 4 IS NOW TWO GENERATED FAMILIES, not one.
467
+ *
468
+ * This assertion compared the tier-4 count against `MODEL_LEAF_COUNT` alone,
469
+ * and it CAUGHT the part leaves the moment they landed — 968 against an
470
+ * expected 216 — which is precisely the job it was written for. Widening it to
471
+ * the sum is the fix; deleting it or loosening it to `>=` would retire the one
472
+ * check that can see a generated map silently empty.
473
+ *
474
+ * Kept as an exact equality for the same reason: a `>=` would pass with
475
+ * `PART_LEAVES` entirely gone.
476
+ */
477
+ /*
478
+ * 🛑 DISTINCT IDS. This assertion did not exist, and its absence is what let 20
479
+ * part leaves overwrite 20 model leaves in production.
480
+ *
481
+ * The whole failure was silent at every layer: the forest walked 1,138 nodes
482
+ * with only 1,118 distinct ids, `appkit-seed load` is a `set(…, {merge:true})`
483
+ * so the second row with an id quietly replaced the first, and the CLI reported
484
+ * `created 1173, errors 0` while Firestore held 1,153 documents. A buyer opening
485
+ * `/categories/category-galeon` got the attack ring instead of the bey.
486
+ *
487
+ * Counting rows is not counting documents, and this is the one check that can
488
+ * tell the difference BEFORE anything is written.
489
+ */
490
+ const allIds = allCategoryRows.map((c) => c.id).filter((x) => typeof x === "string");
491
+ const duplicateIds = [...new Set(allIds.filter((id, i) => allIds.indexOf(id) !== i))];
492
+ if (duplicateIds.length > 0) {
493
+ throw new Error(`category seed: ${duplicateIds.length} duplicate id(s) — a merge write would ` +
494
+ `silently overwrite, so the load would report success and lose rows. ` +
495
+ `First few: ${duplicateIds.slice(0, 8).join(", ")}`);
496
+ }
497
+ if (allIds.length !== allCategoryRows.length) {
498
+ throw new Error(`category seed: ${allCategoryRows.length - allIds.length} row(s) have no string id`);
499
+ }
500
+ const tier4Rows = rawCategories.filter((c) => c.tier === 4).length;
501
+ const expectedTier4 = MODEL_LEAF_COUNT + PART_LEAF_COUNT;
502
+ if (tier4Rows !== expectedTier4) {
503
+ throw new Error(`category seed: ${tier4Rows} tier-4 leaves reached the tree but the generated ` +
504
+ `maps declare ${expectedTier4} (${MODEL_LEAF_COUNT} models + ${PART_LEAF_COUNT} parts). ` +
505
+ `A parent id in the forest no longer matches a key in MODEL_LEAVES or PART_LEAVES — ` +
506
+ `re-run node scripts/gen-category-models.mjs and node scripts/gen-category-parts.mjs ` +
507
+ `and check the parent ids.`);
470
508
  }
471
509
  if (brandRows.length !== BRAND_ROW_COUNT) {
472
510
  throw new Error(`category seed: ${brandRows.length} brand rows built, expected ${BRAND_ROW_COUNT}`);
@@ -446,20 +446,62 @@ export const homepageSectionsSeedData = [
446
446
  title: "Frequently Asked Questions",
447
447
  subtitle: "The questions new buyers ask most.",
448
448
  showOnHomepage: true,
449
- displayCount: 6,
449
+ /*
450
+ * B6b — 6 → 10. The FAQ block is the densest single SEO win on the page
451
+ * (worldhobbyshop's 10-question block is a measured part of why it leads
452
+ * the fifteen on SEO) and it is an answer-engine surface, so it feeds
453
+ * `faqJsonLd`. 20 rows are flagged `showOnHomepage`; showing 10 of them
454
+ * is the intended shape.
455
+ */
456
+ displayCount: 10,
450
457
  linkToFullPage: true,
451
458
  viewMoreLabel: "View all FAQs →",
452
459
  showCategoryTabs: true,
453
- visibleTabs: ["shipping_delivery", "returns_refunds", "orders_payment"],
460
+ /*
461
+ * 🛑 EMPTY ON PURPOSE — the tabs DERIVE from the items actually shown.
462
+ *
463
+ * This listed `["shipping_delivery", "returns_refunds",
464
+ * "orders_payment"]` and measured live on 2026-10-12 **all three
465
+ * rendered EMPTY PANELS**, while the three categories whose rows were
466
+ * actually displayed (`general`, `product_information`,
467
+ * `fees_and_taxes`) had no tab at all.
468
+ *
469
+ * The cause is that the two halves answer different questions and
470
+ * neither knows about the other: the ITEMS come from
471
+ * `getHomepageFAQs()` — `showOnHomepage` ordered by `priority` DESC,
472
+ * then sliced to `displayCount` — while the TABS were a hand-written
473
+ * list. Raise a row's priority, or add a higher-priority row in another
474
+ * category (B6b's FAQ half added 22, including a whole new
475
+ * `fees_and_taxes`), and the slice moves out from under the tabs
476
+ * silently. That is Root Cause #72's shape: a tab you can click with
477
+ * nothing behind it.
478
+ *
479
+ * `FAQSection` already derives tabs from its items when `tabs` is empty
480
+ * (`if (tabsProp && tabsProp.length > 0) return tabsProp;`), so an empty
481
+ * array makes an empty tab STRUCTURALLY IMPOSSIBLE rather than something
482
+ * to re-sync by hand. A hand-maintained enumeration beside a query is
483
+ * the defect, not the list's contents.
484
+ *
485
+ * 🛑 `[]` rather than deleting the key, because `appkit-seed load` is a
486
+ * MERGE write and cannot remove a field — dropping it from the seed
487
+ * would leave the three stale values in Firestore forever and the fix
488
+ * would read as applied while changing nothing.
489
+ */
490
+ visibleTabs: [],
454
491
  allowMultipleOpen: false,
455
492
  defaultOpenCount: 1,
456
- categories: [
457
- "general",
458
- "orders_payment",
459
- "shipping_delivery",
460
- "returns_refunds",
461
- "product_information",
462
- ],
493
+ /*
494
+ * 🛑 NOT a filter, despite the name. The renderer uses this ONLY as a
495
+ * fallback tab list when `visibleTabs` is empty
496
+ * (`cfg?.visibleTabs?.length ? cfg.visibleTabs : cfg?.categories`), and
497
+ * the items are never filtered by it. So a category absent from here is
498
+ * still displayed — which is why `fees_and_taxes` rows appear on the
499
+ * homepage today while being absent from this list.
500
+ *
501
+ * Emptied too, or it would simply become the hand-written tab list this
502
+ * change exists to delete.
503
+ */
504
+ categories: [],
463
505
  },
464
506
  createdAt: daysAgo(90),
465
507
  updatedAt: daysAgo(16),
@@ -517,4 +559,123 @@ export const homepageSectionsSeedData = [
517
559
  * array does not remove them from Firestore — delete both documents at
518
560
  * /admin/sections after seeding, or they keep rendering.
519
561
  */
562
+ /*
563
+ * ── B6b — THE SYSTEMS MATRIX ─────────────────────────────────────────────
564
+ *
565
+ * worldhobbyshop's single biggest SEO asset, and it is TAXONOMY ROUTING
566
+ * rather than merchandising: 25 internal links telling a crawler which
567
+ * sub-collections exist. It is most of why they have 79 unique internal
568
+ * links (BAS has 57) while running the FEWEST products and words on their
569
+ * homepage of the fifteen crawled — 8 product cards and 1,816 words.
570
+ *
571
+ * 🛑 `custom-cards`, NOT `collection-cards`, and the plan had this wrong.
572
+ * `collection-cards` is capped at `COLLECTION_CARDS_MAX_ENTRIES = 3` and its
573
+ * entries are LIVE-CONTENT queries (`{type, limit, categorySlug}`), so it
574
+ * cannot express a link matrix at all. `custom-cards` carries
575
+ * `cards[].buttons[]` of `{label, href}`, which is exactly a matrix.
576
+ *
577
+ * 🛑 Every href is a REAL post-B5 category id, read off `categoriesSeedData`
578
+ * rather than typed from memory. A dead link here is the Root Cause #33
579
+ * shape on the routing axis — it 404s, and `audit-nav-page-wiring` does not
580
+ * reach section configs.
581
+ *
582
+ * No prices anywhere, which is what makes this safe where a price-band tile
583
+ * is not — see the note below the array.
584
+ */
585
+ {
586
+ id: "section-systems-matrix",
587
+ type: "custom-cards",
588
+ order: 5,
589
+ enabled: true,
590
+ config: {
591
+ title: "Every system, every part family",
592
+ layout: "grid",
593
+ columns: 4,
594
+ autoScroll: false,
595
+ loop: false,
596
+ cards: [
597
+ {
598
+ id: "matrix-original",
599
+ eyebrow: "1999–2003",
600
+ title: "Beyblade Original",
601
+ body: "Four part systems in four years, and they do not interchange. Plastic Generation through Heavy Metal System.",
602
+ buttons: [
603
+ { label: "Plastic Generation", href: "/categories/category-original-plastic-gen", variant: "ghost" },
604
+ { label: "Spin Gear", href: "/categories/category-original-spin-gear", variant: "ghost" },
605
+ { label: "Magnacore", href: "/categories/category-original-magnacore", variant: "ghost" },
606
+ { label: "Engine Gear", href: "/categories/category-original-engine-gear", variant: "ghost" },
607
+ { label: "Heavy Metal System", href: "/categories/category-original-hms", variant: "ghost" },
608
+ { label: "Parts", href: "/categories/category-original-parts", variant: "outline" },
609
+ { label: "Launchers", href: "/categories/category-original-launchers", variant: "outline" },
610
+ ],
611
+ },
612
+ {
613
+ id: "matrix-metal",
614
+ eyebrow: "2008–2013",
615
+ title: "Metal Fight",
616
+ body: "The tuning era, and the only generation where loose parts outnumber complete tops.",
617
+ buttons: [
618
+ { label: "Metal System", href: "/categories/category-metal-phws", variant: "ghost" },
619
+ { label: "Metal Fusion", href: "/categories/category-metal-fusion", variant: "ghost" },
620
+ { label: "Metal Masters", href: "/categories/category-metal-masters", variant: "ghost" },
621
+ { label: "Metal Fury (4D)", href: "/categories/category-metal-fury-4d", variant: "ghost" },
622
+ { label: "Shogun Steel", href: "/categories/category-metal-shogun-steel", variant: "ghost" },
623
+ { label: "Parts", href: "/categories/category-metal-parts", variant: "outline" },
624
+ { label: "Launchers", href: "/categories/category-metal-launchers", variant: "outline" },
625
+ ],
626
+ },
627
+ {
628
+ id: "matrix-burst",
629
+ eyebrow: "2015–2023",
630
+ title: "Beyblade Burst",
631
+ body: "Seven waves of the bursting mechanic, from single-layer Classic to Burst Ultimate.",
632
+ buttons: [
633
+ { label: "Burst Classic", href: "/categories/category-burst-classic", variant: "ghost" },
634
+ { label: "Burst God", href: "/categories/category-burst-god", variant: "ghost" },
635
+ { label: "Cho-Z", href: "/categories/category-burst-cho-z", variant: "ghost" },
636
+ { label: "GT", href: "/categories/category-burst-gt", variant: "ghost" },
637
+ { label: "Superking", href: "/categories/category-burst-superking", variant: "ghost" },
638
+ { label: "Dynamite Battle", href: "/categories/category-burst-db", variant: "ghost" },
639
+ { label: "Burst Ultimate", href: "/categories/category-burst-bu", variant: "ghost" },
640
+ { label: "Parts", href: "/categories/category-burst-parts", variant: "outline" },
641
+ ],
642
+ },
643
+ {
644
+ id: "matrix-x",
645
+ eyebrow: "2023–",
646
+ title: "Beyblade X",
647
+ body: "The Xtreme Line rail is the centre of the design — blades, ratchets and bits, and a stadium that is part of the game.",
648
+ buttons: [
649
+ { label: "Basic Line", href: "/categories/category-x-basic", variant: "ghost" },
650
+ { label: "Unique Line (UX)", href: "/categories/category-x-unique-ux", variant: "ghost" },
651
+ { label: "Custom Line (CX)", href: "/categories/category-x-custom-cx", variant: "ghost" },
652
+ { label: "X-Over Project", href: "/categories/category-x-over", variant: "ghost" },
653
+ { label: "Parts", href: "/categories/category-x-parts", variant: "outline" },
654
+ { label: "Launchers", href: "/categories/category-x-launchers", variant: "outline" },
655
+ { label: "Stadiums", href: "/categories/category-x-stadiums", variant: "outline" },
656
+ ],
657
+ },
658
+ ],
659
+ },
660
+ createdAt: daysAgo(1),
661
+ updatedAt: daysAgo(1),
662
+ },
663
+ /*
664
+ * 🛑 TCI's five BUDGET TILES ("Under ₹499 / ₹799 / ₹999 / ₹1499") are
665
+ * DELIBERATELY NOT SEEDED, and this is a decision rather than an omission.
666
+ *
667
+ * The plan argued they are safe because "the tiles are LINKS, not prices".
668
+ * That is true of the tile and false of the mechanism. Prices are hidden from
669
+ * signed-out visitors, and the price-range facet and every money sort are
670
+ * withheld specifically because "hiding the number is not hiding the value" —
671
+ * bracketing a hidden price with a range filter is the one casually reachable
672
+ * way around the gate. The documented residue is that a HAND-WRITTEN
673
+ * `?f=price<499` still works, and five tiles on the homepage promote that
674
+ * residue to the most prominent path on the site: a guest could bucket the
675
+ * entire catalogue into five price bands in five clicks, without an account.
676
+ *
677
+ * The matrix above has no such property — a category link reveals no amount.
678
+ * Revisit if guest price-gating is ever relaxed; until then this is a tile
679
+ * that would quietly undo a gate another step built.
680
+ */
520
681
  ];
@@ -90,3 +90,5 @@ export { mergeFirestoreIndices, generateMergedFirestoreIndexFile, } from "./fire
90
90
  export { assertPiiRoundTrip, seedForTest } from "./test-utils";
91
91
  export type { TestSeedHandles } from "./test-utils";
92
92
  export { type SeedPurpose, type SeedPurposeFields, type WithSeedPurpose, SEED_PURPOSE_VALUES, ALL_SEED_PURPOSES, SEED_COLLECTION_PURPOSE, SEED_TEST_TTL_DAYS, isSeedPurpose, resolveSeedPurpose, seedPurposeFields, stampSeedPurpose, asSeedPurpose, } from "./seed-purpose";
93
+ export { partGlossarySeedData } from "./part-glossary-seed-data";
94
+ export { modelCompositionSeedData } from "./model-composition-seed-data";
@@ -35,6 +35,16 @@ export { storesSeedData } from "./stores-seed-data.js";
35
35
  export { storeAddressesSeedData } from "./store-addresses-seed-data.js";
36
36
  export { categoriesSeedData, categoriesP1SeedData } from "./categories-seed-data.js";
37
37
  export { productsStandardSeedData } from "./products-standard-seed-data.js";
38
+ /*
39
+ * 🛑 `productsCatalogueSeedData` DELETED (I6, 2026-10-13) — 968 generated
40
+ * listings, one per tier-4 leaf. "I do not need products ... rest is either
41
+ * standard or test data."
42
+ *
43
+ * A merge-write loader CANNOT remove a document, so deleting the file alone
44
+ * leaves all 968 rows live and `appkit-seed status` reports the collection in
45
+ * sync either way (it counts ids). The removal is
46
+ * npx appkit-seed delete --yes --collections products && npx appkit-seed load
47
+ */
38
48
  export { productsAuctionsSeedData } from "./products-auctions-seed-data.js";
39
49
  export { productsPreordersSeedData } from "./products-preorders-seed-data.js";
40
50
  export { productsPrizeDrawsSeedData } from "./products-prize-draws-seed-data.js";
@@ -92,3 +102,6 @@ export { assertPiiRoundTrip, seedForTest } from "./test-utils.js";
92
102
  * must never be hand-written beside it (`audit-seed-purpose` blocks it).
93
103
  */
94
104
  export { SEED_PURPOSE_VALUES, ALL_SEED_PURPOSES, SEED_COLLECTION_PURPOSE, SEED_TEST_TTL_DAYS, isSeedPurpose, resolveSeedPurpose, seedPurposeFields, stampSeedPurpose, asSeedPurpose, } from "./seed-purpose.js";
105
+ // E4/D12 — generated from `docs/research/part-composition-2026-10/`.
106
+ export { partGlossarySeedData } from "./part-glossary-seed-data.js";
107
+ export { modelCompositionSeedData } from "./model-composition-seed-data.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * GENERATED by `scripts/generate-parts-seed.mjs` — do not hand-edit.
3
+ * Source: `docs/research/part-composition-2026-10/` (E4a/E4b), itself
4
+ * extracted from the LOCAL corpus with no network crawl.
5
+ *
6
+ * Model composition — which parts each bey is made of — 259 rows.
7
+ *
8
+ * 🛑 Facts only. Fandom is CC-BY-SA, so names, codes, weights and slot
9
+ * structure are taken and the PROSE is not — our own descriptions come from
10
+ * the category templates. Regenerate rather than edit: a hand-fix here is lost
11
+ * on the next run and, worse, makes the committed output disagree with the
12
+ * script that is supposed to produce it.
13
+ */
14
+ import type { ModelCompositionDocument } from "../features/parts/schemas/firestore";
15
+ export declare const modelCompositionSeedData: Partial<ModelCompositionDocument>[];