@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
@@ -656,6 +656,58 @@ const _rawProductsStandardSeedData = [
656
656
  createdAt: new Date("2026-08-24"),
657
657
  updatedAt: new Date("2026-08-24"),
658
658
  },
659
+ /*
660
+ * B6 — THE ONLY 18% GST ROW IN THE CATALOGUE.
661
+ *
662
+ * 🛑 `tax-hsn-95030010` (electronic toys, 18%) has existed as a `taxCodes`
663
+ * row since B3 with nothing referencing it, so the 18% arm of
664
+ * `calculateGst`, `gstByRate` (C0c defect 4), the per-rate invoice table and
665
+ * the blended-rate payout all had NO DATA that could exercise them. Four
666
+ * code paths that cannot be wrong because nothing reaches them.
667
+ *
668
+ * A BeyPointer is a battery-powered scoring device, which is what 9503 0010
669
+ * covers — so this is a genuinely electronic good rather than a flag planted
670
+ * on a convenient row.
671
+ *
672
+ * 🛑 No `gstRate`/`hsnCode` HERE, deliberately. They derive from
673
+ * `category-accessory-counters` through `deriveTaxonomy`, which is the path
674
+ * that will carry them in production. Hand-writing 18 would satisfy step 1 of
675
+ * the resolution order and leave the DERIVATION untested — a fixture that
676
+ * passes while the mechanism it exists to prove is never run, which is the
677
+ * shape of Root Cause #101.
678
+ *
679
+ * It is also what makes a MIXED-RATE cart possible for the first time: this
680
+ * row at 18% beside any bey at 5% is the one case C0c's §13 check 6b asks for
681
+ * and the catalogue could not previously express.
682
+ */
683
+ {
684
+ id: "product-beypointer-digital-battle-counter",
685
+ slug: "product-beypointer-digital-battle-counter",
686
+ barcodeId: "LIR-BEY-ACC-018",
687
+ title: "BeyPointer Digital Battle Counter",
688
+ description: "Battery-powered digital battle counter used for tournament scoring — tracks points across a best-of series, with a reset and a hold. Takes two AAA cells (not included). An electronic accessory, so it is taxed at the 18% electronic-toy rate rather than the 5% that applies to tops and parts.",
689
+ categorySlugs: ["category-accessory-counters", "category-accessories", "category-spinning-tops"],
690
+ categoryNames: ["Counters & BeyPointers", "Accessories", "Spinning Tops"],
691
+ brandSlug: "brand-takara-tomy",
692
+ brand: "Takara-Tomy",
693
+ price: 1299,
694
+ currency: "INR",
695
+ stockQuantity: 6,
696
+ availableQuantity: 6,
697
+ isSold: false,
698
+ mainImage: seedPhoto("product-image-beypointer-digital-battle-counter-1-20261012", 900, 900),
699
+ images: [
700
+ seedPhoto("product-image-beypointer-digital-battle-counter-1-20261012", 900, 900),
701
+ seedPhoto("product-image-beypointer-digital-battle-counter-2-20261012", 900, 900),
702
+ ],
703
+ status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
704
+ condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
705
+ tags: ["accessory"],
706
+ storeId: "store-beyblade-arena",
707
+ storeName: "Beyblade Arena",
708
+ createdAt: new Date("2026-10-12"),
709
+ updatedAt: new Date("2026-10-12"),
710
+ },
659
711
  ];
660
712
  export const productsStandardSeedData = [
661
713
  ..._rawProductsStandardSeedData.map((p) => withTokens({
@@ -133,15 +133,111 @@ export declare const PURGEABLE: Record<SeedPurpose, boolean>;
133
133
  * command, and the cost of being unable to clear it is the state this replaced.
134
134
  */
135
135
  export declare const SEED_COLLECTION_PURPOSE: Record<string, SeedPurpose>;
136
+ /**
137
+ * Individual rows that differ from their collection's default, by document id.
138
+ *
139
+ * 🛑 This is the "clear all out EXCEPT our admin creds and the original single
140
+ * store" instruction, made declarative. Both collections default to `test` —
141
+ * every persona and every other store is disposable — and exactly two rows are
142
+ * not.
143
+ *
144
+ * ✅ The precedent already existed and this replaces it: `cmdDelete` has
145
+ * carried `PROTECTED_USER_UIDS = new Set(["user-admin-letitrip"])` for a long
146
+ * time, skipping that uid on a wholesale delete. One mechanism is better than a
147
+ * list the purge path happens to know about privately.
148
+ *
149
+ * 🛑 Keyed **`"<collection>:<id>"`**, and the collection half is load-bearing.
150
+ *
151
+ * It was a bare id, and a live read-back caught the consequence: `carts`,
152
+ * `wishlists` and `history` are all keyed on the USER id (CLAUDE.md: "Doc id =
153
+ * userId"), so `"user-admin-letitrip"` matched the admin's **cart** as well as
154
+ * the admin's user row — making a disposable cart permanently unpurgeable.
155
+ * Subtle, silent, and it would have left a stale cart surviving every purge
156
+ * forever.
157
+ *
158
+ * The id half is the DOCUMENT id, which for `users` is the Auth uid (`uid` on
159
+ * the seed row) and for everything else is `id`. `resolveSeedPurpose` checks
160
+ * both, because a seeded user row carries `uid` and no `id`.
161
+ */
162
+ export declare const SEED_ROW_PURPOSE: Record<string, SeedPurpose>;
163
+ /**
164
+ * Collections whose purpose FOLLOWS a referenced row rather than being declared.
165
+ *
166
+ * 🛑 This exists to hold one invariant: **a `permanent` row may not reference a
167
+ * `test` row**, because the purge would leave it behind as a dangling FK that
168
+ * still looks like real config.
169
+ *
170
+ * Measured in `store-extensions-seed-data.ts`: **14** rows reference
171
+ * `store-beyblade-arena` (test) and **9** reference `store-letitrip-official`
172
+ * (permanent). So marking `shippingConfigs` / `analyticsCards` permanent at the
173
+ * COLLECTION level — which the user's list implies and an earlier draft of the
174
+ * plan did — would strand 14 rows pointing at a store the purge deletes.
175
+ *
176
+ * Deriving is better than 23 hand-maintained per-row overrides for the usual
177
+ * reason: a hand-kept mirror drifts the moment a store is added (Root Cause
178
+ * #42), and a derivation is correct for stores that do not exist yet.
179
+ */
180
+ export declare const SEED_PURPOSE_DERIVED_FROM: Record<string, {
181
+ readonly field: SeedReferenceField;
182
+ readonly collection: string;
183
+ }>;
136
184
  /** A seeded row may override its collection's default inline. */
137
185
  export interface WithSeedPurpose {
138
186
  seedPurpose?: SeedPurpose;
139
187
  }
140
188
  /**
141
- * The row's own declaration wins; otherwise the collection default; otherwise
142
- * `test`.
189
+ * The fields `resolveSeedPurpose` reads off a row — all optional, because it is
190
+ * handed rows from 47 differently-shaped collections.
191
+ *
192
+ * 🛑 Declared explicitly rather than as `Record<string, unknown>`, which
193
+ * `audit-unknown-leakage` blocks and which is worse anyway: an index signature
194
+ * accepts ANY field name, so a typo in `SEED_PURPOSE_DERIVED_FROM.field` would
195
+ * read `undefined` and fall through to `test` silently — the exact mistake
196
+ * already made once, where `analyticsCards` was declared to derive from
197
+ * `storeId` and the rows carry `ownerId`.
198
+ *
199
+ * `SeedReferenceField` then constrains that `field` to one of these, so the
200
+ * same typo becomes a compile error.
201
+ */
202
+ export interface SeedRowIdentity extends WithSeedPurpose {
203
+ /** Document id, on every collection except `users`. */
204
+ id?: string;
205
+ /** `users` seed rows carry the Auth uid and no `id`. */
206
+ uid?: string;
207
+ storeId?: string;
208
+ ownerId?: string;
209
+ userId?: string;
210
+ sellerId?: string;
211
+ }
212
+ /** The reference fields a derived collection may follow. */
213
+ export type SeedReferenceField = "storeId" | "ownerId" | "userId" | "sellerId";
214
+ /**
215
+ * Resolution order, most specific first:
216
+ *
217
+ * 1. the row's own inline `seedPurpose`
218
+ * 2. a per-row override in `SEED_ROW_PURPOSE`, by `id` or `uid`
219
+ * 3. for a DERIVED collection, the purpose of the row it references
220
+ * 4. the collection default
221
+ * 5. `test`
222
+ *
223
+ * 🛑 Step 3 is what holds the referential invariant. A `shippingConfig` for
224
+ * `store-letitrip-official` resolves `permanent`; one for
225
+ * `store-beyblade-arena` resolves `test` and is purged alongside the store it
226
+ * configures, rather than outliving it.
227
+ */
228
+ export declare function resolveSeedPurpose(collection: string, row?: SeedRowIdentity | null,
229
+ /**
230
+ * The document id, when the caller has it and the row does not.
231
+ *
232
+ * 🛑 Required by `loadGeneric`, which does `let { id, ...data } = docData`
233
+ * and therefore hands this function a row with **no `id` at all**. Without
234
+ * this parameter the per-row overrides silently did not apply: measured,
235
+ * `store-letitrip-official` loaded as `test` — i.e. the platform's own store
236
+ * would have been purged, which is exactly the row the keep-list exists to
237
+ * protect. `loadUsers` was unaffected because `{ ...u }` keeps `uid`, which
238
+ * is why the admin override worked and the store one did not.
143
239
  */
144
- export declare function resolveSeedPurpose(collection: string, row?: WithSeedPurpose | null): SeedPurpose;
240
+ docId?: string | null): SeedPurpose;
145
241
  export interface SeedPurposeFields {
146
242
  seedPurpose: SeedPurpose;
147
243
  }
@@ -167,7 +263,10 @@ export declare function seedPurposeFields(purpose: SeedPurpose): SeedPurposeFiel
167
263
  * Returns a NEW object; the seed arrays are module-level constants re-imported
168
264
  * on every CLI invocation and must not be mutated.
169
265
  */
170
- export declare function stampSeedPurpose<T extends WithSeedPurpose>(collection: string, row: T): T & SeedPurposeFields;
266
+ export declare function stampSeedPurpose<T extends WithSeedPurpose>(collection: string, row: T,
267
+ /** Pass this whenever the caller holds the id and the row does not — see
268
+ * `resolveSeedPurpose`'s `docId` for why `loadGeneric` must. */
269
+ docId?: string | null): T & SeedPurposeFields;
171
270
  /**
172
271
  * Mark a whole array as something other than its collection default — the
173
272
  * readable way to say "this file is permanent" (or "this file is fixtures")
@@ -131,6 +131,23 @@ export const SEED_COLLECTION_PURPOSE = {
131
131
  /* Branding, fees, commissions, watermark, legal pages, and the AES-encrypted
132
132
  * credentials. 🛑 A purge here would destroy live API keys. */
133
133
  siteSettings: "permanent",
134
+ /*
135
+ * ── permanent, second tranche: the site's own CONTENT and COMPOSITION ────
136
+ *
137
+ * Added on the user's explicit list. The same test applies and they pass it:
138
+ * clearing any of these means re-AUTHORING, not re-running a loader.
139
+ *
140
+ * 🛑 But "permanent" obliges us to make them REAL — see G1c in the plan.
141
+ * Marking a placeholder permanent only makes the placeholder unpurgeable, so
142
+ * each of these has to trace to a crawl source: blogPosts to the buying/rules
143
+ * guide split, homepageSections to worldhobbyshop's generation tiles and
144
+ * 25-link systems matrix, carouselSlides to beybladeartshop's 5-slide hero
145
+ * including the anti-counterfeit slide.
146
+ */
147
+ blogPosts: "permanent",
148
+ carousels: "permanent",
149
+ carouselSlides: "permanent",
150
+ homepageSections: "permanent",
134
151
  /*
135
152
  * 🛑 The tester checklist cases are PERMANENT CONTENT, not scaffolding.
136
153
  *
@@ -145,6 +162,15 @@ export const SEED_COLLECTION_PURPOSE = {
145
162
  * default is for.
146
163
  */
147
164
  testerChecklistItems: "permanent",
165
+ /*
166
+ * E4/D12 — PERMANENT. These are extracted from a corpus by committed
167
+ * scripts, not fixtures: clearing them means re-running an extraction over a
168
+ * 7 GB local crawl, which is the test § G1c sets ("if clearing it means
169
+ * somebody has to WRITE it again rather than re-run a loader"). Re-running
170
+ * the extractor is possible but needs a corpus a clone will not have.
171
+ */
172
+ partGlossary: "permanent",
173
+ modelComposition: "permanent",
148
174
  testerChecklistResponses: "test",
149
175
  /*
150
176
  * ── test: everything else ───────────────────────────────────────────────
@@ -172,20 +198,14 @@ export const SEED_COLLECTION_PURPOSE = {
172
198
  notifications: "test",
173
199
  supportTickets: "test",
174
200
  catalogueItems: "test",
175
- blogPosts: "test",
176
201
  events: "test",
177
202
  eventEntries: "test",
178
203
  groupedListings: "test",
179
204
  scammerProfiles: "test",
180
- carousels: "test",
181
- carouselSlides: "test",
182
- homepageSections: "test",
183
205
  procurementShipments: "test",
184
206
  shipmentLots: "test",
185
207
  shipmentItems: "test",
186
208
  payoutMethods: "test",
187
- shippingConfigs: "test",
188
- analyticsCards: "test",
189
209
  analyticsAlerts: "test",
190
210
  storeCategories: "test",
191
211
  listingTemplates: "test",
@@ -195,12 +215,131 @@ export const SEED_COLLECTION_PURPOSE = {
195
215
  storeGoogleConfig: "test",
196
216
  };
197
217
  /**
198
- * The row's own declaration wins; otherwise the collection default; otherwise
199
- * `test`.
218
+ * Individual rows that differ from their collection's default, by document id.
219
+ *
220
+ * 🛑 This is the "clear all out EXCEPT our admin creds and the original single
221
+ * store" instruction, made declarative. Both collections default to `test` —
222
+ * every persona and every other store is disposable — and exactly two rows are
223
+ * not.
224
+ *
225
+ * ✅ The precedent already existed and this replaces it: `cmdDelete` has
226
+ * carried `PROTECTED_USER_UIDS = new Set(["user-admin-letitrip"])` for a long
227
+ * time, skipping that uid on a wholesale delete. One mechanism is better than a
228
+ * list the purge path happens to know about privately.
229
+ *
230
+ * 🛑 Keyed **`"<collection>:<id>"`**, and the collection half is load-bearing.
231
+ *
232
+ * It was a bare id, and a live read-back caught the consequence: `carts`,
233
+ * `wishlists` and `history` are all keyed on the USER id (CLAUDE.md: "Doc id =
234
+ * userId"), so `"user-admin-letitrip"` matched the admin's **cart** as well as
235
+ * the admin's user row — making a disposable cart permanently unpurgeable.
236
+ * Subtle, silent, and it would have left a stale cart surviving every purge
237
+ * forever.
238
+ *
239
+ * The id half is the DOCUMENT id, which for `users` is the Auth uid (`uid` on
240
+ * the seed row) and for everything else is `id`. `resolveSeedPurpose` checks
241
+ * both, because a seeded user row carries `uid` and no `id`.
242
+ */
243
+ export const SEED_ROW_PURPOSE = {
244
+ /* Our own admin account. Clearing it locks us out of our own site — and the
245
+ * Auth record is the half most easily missed, since `appkit-seed delete` does
246
+ * not touch Firebase Auth at all. */
247
+ "users:user-admin-letitrip": "permanent",
248
+ /* The platform's own default store. The other three seeded stores
249
+ * (beyblade-arena, blader-bazaar, vintage-vault-co) are test fixtures. */
250
+ "stores:store-letitrip-official": "permanent",
251
+ };
252
+ /**
253
+ * Collections whose purpose FOLLOWS a referenced row rather than being declared.
254
+ *
255
+ * 🛑 This exists to hold one invariant: **a `permanent` row may not reference a
256
+ * `test` row**, because the purge would leave it behind as a dangling FK that
257
+ * still looks like real config.
258
+ *
259
+ * Measured in `store-extensions-seed-data.ts`: **14** rows reference
260
+ * `store-beyblade-arena` (test) and **9** reference `store-letitrip-official`
261
+ * (permanent). So marking `shippingConfigs` / `analyticsCards` permanent at the
262
+ * COLLECTION level — which the user's list implies and an earlier draft of the
263
+ * plan did — would strand 14 rows pointing at a store the purge deletes.
264
+ *
265
+ * Deriving is better than 23 hand-maintained per-row overrides for the usual
266
+ * reason: a hand-kept mirror drifts the moment a store is added (Root Cause
267
+ * #42), and a derivation is correct for stores that do not exist yet.
268
+ */
269
+ export const SEED_PURPOSE_DERIVED_FROM = {
270
+ shippingConfigs: { field: "storeId", collection: "stores" },
271
+ /*
272
+ * 🛑 `ownerId` → `users`, NOT `storeId` → `stores`. I wrote the store form
273
+ * first and the data disproved it: measured, `analyticsCardsSeedData` carries
274
+ * **zero** `storeId` fields — every row is `{ scope: "seller", ownerId:
275
+ * "user-tyson-blader", … }`. So these follow the USER whose dashboard they
276
+ * compose, and since that user is a test persona all nine correctly resolve
277
+ * `test`.
278
+ *
279
+ * Worth recording because the wrong field does not error: a derived row whose
280
+ * named field is absent falls to `test`, which happened to be the right
281
+ * answer here and would have silently been the WRONG one for a card owned by
282
+ * the admin.
283
+ */
284
+ analyticsCards: { field: "ownerId", collection: "users" },
285
+ };
286
+ /**
287
+ * Resolution order, most specific first:
288
+ *
289
+ * 1. the row's own inline `seedPurpose`
290
+ * 2. a per-row override in `SEED_ROW_PURPOSE`, by `id` or `uid`
291
+ * 3. for a DERIVED collection, the purpose of the row it references
292
+ * 4. the collection default
293
+ * 5. `test`
294
+ *
295
+ * 🛑 Step 3 is what holds the referential invariant. A `shippingConfig` for
296
+ * `store-letitrip-official` resolves `permanent`; one for
297
+ * `store-beyblade-arena` resolves `test` and is purged alongside the store it
298
+ * configures, rather than outliving it.
200
299
  */
201
- export function resolveSeedPurpose(collection, row) {
300
+ export function resolveSeedPurpose(collection, row,
301
+ /**
302
+ * The document id, when the caller has it and the row does not.
303
+ *
304
+ * 🛑 Required by `loadGeneric`, which does `let { id, ...data } = docData`
305
+ * and therefore hands this function a row with **no `id` at all**. Without
306
+ * this parameter the per-row overrides silently did not apply: measured,
307
+ * `store-letitrip-official` loaded as `test` — i.e. the platform's own store
308
+ * would have been purged, which is exactly the row the keep-list exists to
309
+ * protect. `loadUsers` was unaffected because `{ ...u }` keeps `uid`, which
310
+ * is why the admin override worked and the store one did not.
311
+ */
312
+ docId) {
202
313
  if (row && isSeedPurpose(row.seedPurpose))
203
314
  return row.seedPurpose;
315
+ /* `"<collection>:<id>"` — the collection half is what stops a users
316
+ * override also matching that user's CART, which is keyed on the same id. */
317
+ const rowKey = (coll, id) => `${coll}:${id}`;
318
+ if (docId && isSeedPurpose(SEED_ROW_PURPOSE[rowKey(collection, docId)]))
319
+ return SEED_ROW_PURPOSE[rowKey(collection, docId)];
320
+ if (row) {
321
+ /* `users` seed rows carry `uid` and no `id`; everything else carries `id`. */
322
+ const ownId = typeof row.id === "string" ? row.id : typeof row.uid === "string" ? row.uid : null;
323
+ if (ownId && isSeedPurpose(SEED_ROW_PURPOSE[rowKey(collection, ownId)]))
324
+ return SEED_ROW_PURPOSE[rowKey(collection, ownId)];
325
+ const derived = SEED_PURPOSE_DERIVED_FROM[collection];
326
+ if (derived) {
327
+ const refId = row[derived.field];
328
+ if (typeof refId === "string") {
329
+ const refOverride = SEED_ROW_PURPOSE[rowKey(derived.collection, refId)];
330
+ if (isSeedPurpose(refOverride))
331
+ return refOverride;
332
+ const refDefault = SEED_COLLECTION_PURPOSE[derived.collection];
333
+ return isSeedPurpose(refDefault) ? refDefault : "test";
334
+ }
335
+ /*
336
+ * A derived row with no reference cannot inherit, and guessing
337
+ * `permanent` would be the dangling-FK failure this map exists to
338
+ * prevent. `test` is the safe answer: it is re-seedable.
339
+ */
340
+ return "test";
341
+ }
342
+ }
204
343
  const declared = SEED_COLLECTION_PURPOSE[collection];
205
344
  return isSeedPurpose(declared) ? declared : "test";
206
345
  }
@@ -228,8 +367,20 @@ export function seedPurposeFields(purpose) {
228
367
  * Returns a NEW object; the seed arrays are module-level constants re-imported
229
368
  * on every CLI invocation and must not be mutated.
230
369
  */
231
- export function stampSeedPurpose(collection, row) {
232
- return { ...row, ...seedPurposeFields(resolveSeedPurpose(collection, row)) };
370
+ export function stampSeedPurpose(collection, row,
371
+ /** Pass this whenever the caller holds the id and the row does not — see
372
+ * `resolveSeedPurpose`'s `docId` for why `loadGeneric` must. */
373
+ docId) {
374
+ /*
375
+ * The cast is to `Record<string, unknown>` only, which is a WIDENING — every
376
+ * object satisfies it structurally, and `resolveSeedPurpose` reads `id`,
377
+ * `uid` and the derived `field` with `typeof` guards rather than trusting the
378
+ * type. 🛑 Deliberately NOT `as any`: that would silence the `collection`
379
+ * argument's check too, which is the mistake the plan records at the
380
+ * `sendNotification`/`as never` sites.
381
+ */
382
+ const purpose = resolveSeedPurpose(collection, row, docId);
383
+ return { ...row, ...seedPurposeFields(purpose) };
233
384
  }
234
385
  /**
235
386
  * Mark a whole array as something other than its collection default — the
@@ -93,7 +93,16 @@ export interface FaqJsonLdInput {
93
93
  }
94
94
  export interface BreadcrumbJsonLdItem {
95
95
  name: string;
96
- url: string;
96
+ /**
97
+ * Absent on the LAST crumb — the page you are already on.
98
+ *
99
+ * 🛑 This was required, and `url.startsWith(...)` threw on a missing one. It
100
+ * is optional because schema.org's own breadcrumb guidance is that the final
101
+ * element identifies the current page and its `item` may be omitted; a
102
+ * self-link there is accepted but says nothing. Every existing caller passes
103
+ * a url, so widening it breaks none of them.
104
+ */
105
+ url?: string;
97
106
  }
98
107
  export interface BlogPostJsonLdInput {
99
108
  title: string;
@@ -174,7 +174,15 @@ export function breadcrumbJsonLd(items) {
174
174
  "@type": "ListItem",
175
175
  position: index + 1,
176
176
  name: item.name,
177
- item: item.url.startsWith("http") ? item.url : `${SITE_URL}${item.url}`,
177
+ /*
178
+ * `item` is OMITTED rather than emitted empty when there is no url.
179
+ * `item: ""` would resolve to the site root, so every page's final crumb
180
+ * would link to the homepage — a wrong link is worse than an absent one,
181
+ * and absent is what the spec asks for on the current page.
182
+ */
183
+ ...(item.url
184
+ ? { item: item.url.startsWith("http") ? item.url : `${SITE_URL}${item.url}` }
185
+ : {}),
178
186
  })),
179
187
  };
180
188
  }
package/dist/server.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export { isAuthError } from "./utils/auth-error";
2
+ export { searchEverything } from "./_internal/server/features/search/search-everything";
3
+ export type { SearchHit, SearchGroup, SearchSource, SearchEverythingResult, } from "./_internal/server/features/search/search-everything";
4
+ export { SEARCH_SOURCES } from "./_internal/server/features/search/search-sources";
5
+ export { getPartByKey, getModelsUsingPart, getCompositionById, getPartsForComposition, getCompositionsForCategory, listPartsBySlot, } from "./_internal/server/features/parts/data";
6
+ export { renderPartOg, renderPartClassificationOg, } from "./_internal/server/features/parts/og";
7
+ export { assembleFromComponents, partOutToComponents, planAssembly, assertAssemblyInput, MAX_ASSEMBLY_COMPONENTS, } from "./_internal/server/features/parts/assembly";
8
+ export type { AssemblyComponent, AssembleInput, PartOutInput, AssemblyResult, AssemblyPlan, StockWrite, } from "./_internal/server/features/parts/assembly";
2
9
  export { safeRead, installDegradedReadReporter, } from "./errors/safe-read";
3
10
  export type { DegradedReadReport } from "./errors/safe-read";
4
11
  export { ServerErrorsRepository, serverErrorsRepository, } from "./features/server-errors/repository/server-errors.repository";
package/dist/server.js CHANGED
@@ -2,6 +2,17 @@
2
2
  // isAuthError - Pure util — detects auth errors from caught exceptions (usable in server actions).
3
3
  export { isAuthError } from "./utils/auth-error.js";
4
4
  // [SERVER-ONLY] safeRead — wraps optional reads whose failure must not break a page.
5
+ // D2 Fix 4 — cross-resource search. Exported from `server.ts` because it takes
6
+ // a Firestore handle and runs only server-side; `_internal/` is not an
7
+ // exported subpath, and a consumer page must not reach into it.
8
+ export { searchEverything } from "./_internal/server/features/search/search-everything.js";
9
+ export { SEARCH_SOURCES } from "./_internal/server/features/search/search-sources.js";
10
+ // D12 — part + composition reads. Every one is by document id or ONE
11
+ // indexed equality; none needs a composite index.
12
+ export { getPartByKey, getModelsUsingPart, getCompositionById, getPartsForComposition, getCompositionsForCategory, listPartsBySlot, } from "./_internal/server/features/parts/data.js";
13
+ export { renderPartOg, renderPartClassificationOg, } from "./_internal/server/features/parts/og.js";
14
+ // D13 — assembly and part-out. Both run in a real transaction that RE-READS.
15
+ export { assembleFromComponents, partOutToComponents, planAssembly, assertAssemblyInput, MAX_ASSEMBLY_COMPONENTS, } from "./_internal/server/features/parts/assembly.js";
5
16
  // Persists DEGRADED_READ rows via the installed reporter; returns typed fallback on error.
6
17
  export { safeRead, installDegradedReadReporter, } from "./errors/safe-read.js";
7
18
  // [SERVER-ONLY] serverErrorsRepository — Firestore-backed persistent log for 5xx + selected 4xx.