@voyant-travel/catalog 0.117.2

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 (243) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +190 -0
  3. package/dist/adapter/booking-forwarding.d.ts +2 -0
  4. package/dist/adapter/booking-forwarding.d.ts.map +1 -0
  5. package/dist/adapter/booking-forwarding.js +1 -0
  6. package/dist/adapter/channel-push-contracts.d.ts +2 -0
  7. package/dist/adapter/channel-push-contracts.d.ts.map +1 -0
  8. package/dist/adapter/channel-push-contracts.js +1 -0
  9. package/dist/adapter/contract.d.ts +2 -0
  10. package/dist/adapter/contract.d.ts.map +1 -0
  11. package/dist/adapter/contract.js +1 -0
  12. package/dist/adapter/contract.test.d.ts +2 -0
  13. package/dist/adapter/contract.test.d.ts.map +1 -0
  14. package/dist/adapter/contract.test.js +390 -0
  15. package/dist/adapter/provider-contracts.d.ts +2 -0
  16. package/dist/adapter/provider-contracts.d.ts.map +1 -0
  17. package/dist/adapter/provider-contracts.js +1 -0
  18. package/dist/adapter/provider-contracts.test.d.ts +2 -0
  19. package/dist/adapter/provider-contracts.test.d.ts.map +1 -0
  20. package/dist/adapter/provider-contracts.test.js +206 -0
  21. package/dist/adapter/schemas.d.ts +2 -0
  22. package/dist/adapter/schemas.d.ts.map +1 -0
  23. package/dist/adapter/schemas.js +1 -0
  24. package/dist/adapter/schemas.test.d.ts +2 -0
  25. package/dist/adapter/schemas.test.d.ts.map +1 -0
  26. package/dist/adapter/schemas.test.js +344 -0
  27. package/dist/booking-engine/book.d.ts +124 -0
  28. package/dist/booking-engine/book.d.ts.map +1 -0
  29. package/dist/booking-engine/book.js +311 -0
  30. package/dist/booking-engine/cancel.d.ts +40 -0
  31. package/dist/booking-engine/cancel.d.ts.map +1 -0
  32. package/dist/booking-engine/cancel.js +56 -0
  33. package/dist/booking-engine/checkout-finalize.d.ts +146 -0
  34. package/dist/booking-engine/checkout-finalize.d.ts.map +1 -0
  35. package/dist/booking-engine/checkout-finalize.js +132 -0
  36. package/dist/booking-engine/contracts.d.ts +9 -0
  37. package/dist/booking-engine/contracts.d.ts.map +1 -0
  38. package/dist/booking-engine/contracts.js +8 -0
  39. package/dist/booking-engine/contracts.test.d.ts +2 -0
  40. package/dist/booking-engine/contracts.test.d.ts.map +1 -0
  41. package/dist/booking-engine/contracts.test.js +116 -0
  42. package/dist/booking-engine/draft-shape.d.ts +10 -0
  43. package/dist/booking-engine/draft-shape.d.ts.map +1 -0
  44. package/dist/booking-engine/draft-shape.js +9 -0
  45. package/dist/booking-engine/draft-shape.test.d.ts +2 -0
  46. package/dist/booking-engine/draft-shape.test.d.ts.map +1 -0
  47. package/dist/booking-engine/draft-shape.test.js +74 -0
  48. package/dist/booking-engine/drafts-schema.d.ts +302 -0
  49. package/dist/booking-engine/drafts-schema.d.ts.map +1 -0
  50. package/dist/booking-engine/drafts-schema.js +53 -0
  51. package/dist/booking-engine/drafts-service.d.ts +41 -0
  52. package/dist/booking-engine/drafts-service.d.ts.map +1 -0
  53. package/dist/booking-engine/drafts-service.js +108 -0
  54. package/dist/booking-engine/errors.d.ts +81 -0
  55. package/dist/booking-engine/errors.d.ts.map +1 -0
  56. package/dist/booking-engine/errors.js +113 -0
  57. package/dist/booking-engine/index.d.ts +36 -0
  58. package/dist/booking-engine/index.d.ts.map +1 -0
  59. package/dist/booking-engine/index.js +34 -0
  60. package/dist/booking-engine/orders.d.ts +41 -0
  61. package/dist/booking-engine/orders.d.ts.map +1 -0
  62. package/dist/booking-engine/orders.js +49 -0
  63. package/dist/booking-engine/owned-handler.d.ts +166 -0
  64. package/dist/booking-engine/owned-handler.d.ts.map +1 -0
  65. package/dist/booking-engine/owned-handler.js +50 -0
  66. package/dist/booking-engine/owned-handler.test.d.ts +2 -0
  67. package/dist/booking-engine/owned-handler.test.d.ts.map +1 -0
  68. package/dist/booking-engine/owned-handler.test.js +63 -0
  69. package/dist/booking-engine/promotions-contract.d.ts +8 -0
  70. package/dist/booking-engine/promotions-contract.d.ts.map +1 -0
  71. package/dist/booking-engine/promotions-contract.js +7 -0
  72. package/dist/booking-engine/quote-enricher.test.d.ts +12 -0
  73. package/dist/booking-engine/quote-enricher.test.d.ts.map +1 -0
  74. package/dist/booking-engine/quote-enricher.test.js +138 -0
  75. package/dist/booking-engine/quote.d.ts +163 -0
  76. package/dist/booking-engine/quote.d.ts.map +1 -0
  77. package/dist/booking-engine/quote.js +259 -0
  78. package/dist/booking-engine/registry.d.ts +85 -0
  79. package/dist/booking-engine/registry.d.ts.map +1 -0
  80. package/dist/booking-engine/registry.js +118 -0
  81. package/dist/booking-engine/registry.test.d.ts +2 -0
  82. package/dist/booking-engine/registry.test.d.ts.map +1 -0
  83. package/dist/booking-engine/registry.test.js +132 -0
  84. package/dist/booking-engine/routes-contracts.d.ts +169 -0
  85. package/dist/booking-engine/routes-contracts.d.ts.map +1 -0
  86. package/dist/booking-engine/routes-contracts.js +63 -0
  87. package/dist/booking-engine/routes.d.ts +7 -0
  88. package/dist/booking-engine/routes.d.ts.map +1 -0
  89. package/dist/booking-engine/routes.js +443 -0
  90. package/dist/booking-engine/routes.test.d.ts +2 -0
  91. package/dist/booking-engine/routes.test.d.ts.map +1 -0
  92. package/dist/booking-engine/routes.test.js +304 -0
  93. package/dist/booking-engine/schema.d.ts +455 -0
  94. package/dist/booking-engine/schema.d.ts.map +1 -0
  95. package/dist/booking-engine/schema.js +75 -0
  96. package/dist/booking-engine/snapshot-content.d.ts +120 -0
  97. package/dist/booking-engine/snapshot-content.d.ts.map +1 -0
  98. package/dist/booking-engine/snapshot-content.js +110 -0
  99. package/dist/booking-engine/snapshot-content.test.d.ts +2 -0
  100. package/dist/booking-engine/snapshot-content.test.d.ts.map +1 -0
  101. package/dist/booking-engine/snapshot-content.test.js +213 -0
  102. package/dist/booking-engine/sync.d.ts +136 -0
  103. package/dist/booking-engine/sync.d.ts.map +1 -0
  104. package/dist/booking-engine/sync.js +177 -0
  105. package/dist/booking-engine/sync.test.d.ts +2 -0
  106. package/dist/booking-engine/sync.test.d.ts.map +1 -0
  107. package/dist/booking-engine/sync.test.js +377 -0
  108. package/dist/contract.d.ts +2 -0
  109. package/dist/contract.d.ts.map +1 -0
  110. package/dist/contract.js +1 -0
  111. package/dist/contract.test.d.ts +2 -0
  112. package/dist/contract.test.d.ts.map +1 -0
  113. package/dist/contract.test.js +107 -0
  114. package/dist/drift/events.d.ts +2 -0
  115. package/dist/drift/events.d.ts.map +1 -0
  116. package/dist/drift/events.js +1 -0
  117. package/dist/drift/events.test.d.ts +2 -0
  118. package/dist/drift/events.test.d.ts.map +1 -0
  119. package/dist/drift/events.test.js +100 -0
  120. package/dist/embeddings/contract.d.ts +85 -0
  121. package/dist/embeddings/contract.d.ts.map +1 -0
  122. package/dist/embeddings/contract.js +42 -0
  123. package/dist/embeddings/contract.test.d.ts +2 -0
  124. package/dist/embeddings/contract.test.d.ts.map +1 -0
  125. package/dist/embeddings/contract.test.js +30 -0
  126. package/dist/embeddings/gemini.d.ts +110 -0
  127. package/dist/embeddings/gemini.d.ts.map +1 -0
  128. package/dist/embeddings/gemini.js +118 -0
  129. package/dist/embeddings/gemini.test.d.ts +2 -0
  130. package/dist/embeddings/gemini.test.d.ts.map +1 -0
  131. package/dist/embeddings/gemini.test.js +132 -0
  132. package/dist/embeddings/model-registry.d.ts +62 -0
  133. package/dist/embeddings/model-registry.d.ts.map +1 -0
  134. package/dist/embeddings/model-registry.js +78 -0
  135. package/dist/embeddings/model-registry.test.d.ts +2 -0
  136. package/dist/embeddings/model-registry.test.d.ts.map +1 -0
  137. package/dist/embeddings/model-registry.test.js +81 -0
  138. package/dist/embeddings/openai.d.ts +81 -0
  139. package/dist/embeddings/openai.d.ts.map +1 -0
  140. package/dist/embeddings/openai.js +123 -0
  141. package/dist/embeddings/openai.test.d.ts +2 -0
  142. package/dist/embeddings/openai.test.d.ts.map +1 -0
  143. package/dist/embeddings/openai.test.js +164 -0
  144. package/dist/events/taxonomy.d.ts +158 -0
  145. package/dist/events/taxonomy.d.ts.map +1 -0
  146. package/dist/events/taxonomy.js +99 -0
  147. package/dist/events/taxonomy.test.d.ts +2 -0
  148. package/dist/events/taxonomy.test.d.ts.map +1 -0
  149. package/dist/events/taxonomy.test.js +48 -0
  150. package/dist/index.d.ts +27 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +39 -0
  153. package/dist/indexer/contract.d.ts +203 -0
  154. package/dist/indexer/contract.d.ts.map +1 -0
  155. package/dist/indexer/contract.js +16 -0
  156. package/dist/indexer/typesense-search-query.d.ts +31 -0
  157. package/dist/indexer/typesense-search-query.d.ts.map +1 -0
  158. package/dist/indexer/typesense-search-query.js +185 -0
  159. package/dist/indexer/typesense.d.ts +105 -0
  160. package/dist/indexer/typesense.d.ts.map +1 -0
  161. package/dist/indexer/typesense.js +394 -0
  162. package/dist/indexer/typesense.test.d.ts +2 -0
  163. package/dist/indexer/typesense.test.d.ts.map +1 -0
  164. package/dist/indexer/typesense.test.js +253 -0
  165. package/dist/overlay/resolver.d.ts +101 -0
  166. package/dist/overlay/resolver.d.ts.map +1 -0
  167. package/dist/overlay/resolver.js +167 -0
  168. package/dist/overlay/resolver.test.d.ts +2 -0
  169. package/dist/overlay/resolver.test.d.ts.map +1 -0
  170. package/dist/overlay/resolver.test.js +179 -0
  171. package/dist/overlay/schema.d.ts +266 -0
  172. package/dist/overlay/schema.d.ts.map +1 -0
  173. package/dist/overlay/schema.js +71 -0
  174. package/dist/provenance.d.ts +2 -0
  175. package/dist/provenance.d.ts.map +1 -0
  176. package/dist/provenance.js +1 -0
  177. package/dist/schema-sourced-entries.d.ts +344 -0
  178. package/dist/schema-sourced-entries.d.ts.map +1 -0
  179. package/dist/schema-sourced-entries.js +75 -0
  180. package/dist/schema.d.ts +21 -0
  181. package/dist/schema.d.ts.map +1 -0
  182. package/dist/schema.js +20 -0
  183. package/dist/search/federate.d.ts +58 -0
  184. package/dist/search/federate.d.ts.map +1 -0
  185. package/dist/search/federate.js +103 -0
  186. package/dist/search/federate.test.d.ts +2 -0
  187. package/dist/search/federate.test.d.ts.map +1 -0
  188. package/dist/search/federate.test.js +146 -0
  189. package/dist/search/rerank.d.ts +77 -0
  190. package/dist/search/rerank.d.ts.map +1 -0
  191. package/dist/search/rerank.js +68 -0
  192. package/dist/search/rerank.test.d.ts +2 -0
  193. package/dist/search/rerank.test.d.ts.map +1 -0
  194. package/dist/search/rerank.test.js +60 -0
  195. package/dist/search/routes.d.ts +144 -0
  196. package/dist/search/routes.d.ts.map +1 -0
  197. package/dist/search/routes.js +288 -0
  198. package/dist/search/routes.test.d.ts +2 -0
  199. package/dist/search/routes.test.d.ts.map +1 -0
  200. package/dist/search/routes.test.js +322 -0
  201. package/dist/search/semantic.d.ts +63 -0
  202. package/dist/search/semantic.d.ts.map +1 -0
  203. package/dist/search/semantic.js +75 -0
  204. package/dist/search/semantic.test.d.ts +2 -0
  205. package/dist/search/semantic.test.d.ts.map +1 -0
  206. package/dist/search/semantic.test.js +143 -0
  207. package/dist/services/build-indexer-document.test.d.ts +2 -0
  208. package/dist/services/build-indexer-document.test.d.ts.map +1 -0
  209. package/dist/services/build-indexer-document.test.js +102 -0
  210. package/dist/services/content-service.d.ts +125 -0
  211. package/dist/services/content-service.d.ts.map +1 -0
  212. package/dist/services/content-service.js +139 -0
  213. package/dist/services/content-service.test.d.ts +2 -0
  214. package/dist/services/content-service.test.d.ts.map +1 -0
  215. package/dist/services/content-service.test.js +322 -0
  216. package/dist/services/indexer-service.d.ts +109 -0
  217. package/dist/services/indexer-service.d.ts.map +1 -0
  218. package/dist/services/indexer-service.js +123 -0
  219. package/dist/services/indexer-service.test.d.ts +2 -0
  220. package/dist/services/indexer-service.test.d.ts.map +1 -0
  221. package/dist/services/indexer-service.test.js +176 -0
  222. package/dist/services/overlay-service.d.ts +108 -0
  223. package/dist/services/overlay-service.d.ts.map +1 -0
  224. package/dist/services/overlay-service.js +211 -0
  225. package/dist/services/overlay-service.test.d.ts +2 -0
  226. package/dist/services/overlay-service.test.d.ts.map +1 -0
  227. package/dist/services/overlay-service.test.js +79 -0
  228. package/dist/services/snapshot-builder.test.d.ts +2 -0
  229. package/dist/services/snapshot-builder.test.d.ts.map +1 -0
  230. package/dist/services/snapshot-builder.test.js +93 -0
  231. package/dist/services/snapshot-service.d.ts +78 -0
  232. package/dist/services/snapshot-service.d.ts.map +1 -0
  233. package/dist/services/snapshot-service.js +165 -0
  234. package/dist/services/sourced-entry-service.d.ts +142 -0
  235. package/dist/services/sourced-entry-service.d.ts.map +1 -0
  236. package/dist/services/sourced-entry-service.js +203 -0
  237. package/dist/services/sourced-entry-service.test.d.ts +10 -0
  238. package/dist/services/sourced-entry-service.test.d.ts.map +1 -0
  239. package/dist/services/sourced-entry-service.test.js +66 -0
  240. package/dist/snapshot/schema.d.ts +362 -0
  241. package/dist/snapshot/schema.d.ts.map +1 -0
  242. package/dist/snapshot/schema.js +102 -0
  243. package/package.json +210 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-sourced-entries.d.ts","sourceRoot":"","sources":["../src/schema-sourced-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAA;AAEpE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DtC,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG,OAAO,0BAA0B,CAAC,YAAY,CAAA;AACtF,MAAM,MAAM,yBAAyB,GAAG,OAAO,0BAA0B,CAAC,YAAY,CAAA"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Sourced-entry store — durable provenance + projection capture for every
3
+ * non-owned entity that lands in the catalog plane via `discover()`.
4
+ *
5
+ * This is the load-bearing prerequisite for the content cache, the read
6
+ * service, drift invalidation, and snapshot capture. All of them assume
7
+ * there's a durable local row per sourced entity that records *what we
8
+ * know about it locally* — provenance (which adapter, which connection,
9
+ * which upstream id), lifecycle (when first seen, when last seen, whether
10
+ * still active), and the canonical local copy of the indexed projection.
11
+ *
12
+ * Owned entities do NOT have a row here — `provenance.source_kind ===
13
+ * "owned"` reads provenance from the vertical's owned schema. The
14
+ * sourced-entry store is sourced-only.
15
+ *
16
+ * See `docs/architecture/catalog-sourced-content.md` §2.5 for the full
17
+ * design.
18
+ */
19
+ import { typeId } from "@voyant-travel/db/lib/typeid-column";
20
+ import { index, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
21
+ /**
22
+ * `catalog_sourced_entries` — single durable record per sourced entity,
23
+ * keyed two ways: by Voyant-side `(entity_module, entity_id)` for
24
+ * read-path lookup, and by upstream-side `(source_kind,
25
+ * source_connection_id, source_ref)` for discover-time idempotency.
26
+ *
27
+ * The `projection` JSONB column is the canonical local copy of what
28
+ * `adapter.discover()` returned. Read it for thin-content synthesis (when
29
+ * the adapter declares `supportsContentFetch: false`) and for
30
+ * provenance-aware dispatch — never the search index. Search indexes are
31
+ * optimized for full-text/facet queries, not point-reads of rich detail.
32
+ */
33
+ export const catalogSourcedEntriesTable = pgTable("catalog_sourced_entries", {
34
+ id: typeId("catalog_sourced_entries"),
35
+ // Voyant-side identity (which vertical, which entity).
36
+ entity_module: text("entity_module").notNull(),
37
+ entity_id: text("entity_id").notNull(),
38
+ // Provenance — mirrors `Provenance` interface, made durable.
39
+ source_kind: text("source_kind").$type().notNull(),
40
+ source_provider: text("source_provider"),
41
+ source_connection_id: text("source_connection_id"),
42
+ source_ref: text("source_ref"),
43
+ source_freshness: text("source_freshness").$type().notNull(),
44
+ last_sourced_at: timestamp("last_sourced_at", { withTimezone: true }),
45
+ // Lifecycle.
46
+ status: text("status").$type().notNull().default("active"),
47
+ first_seen_at: timestamp("first_seen_at", { withTimezone: true }).notNull().defaultNow(),
48
+ last_seen_at: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
49
+ // Indexed-projection capture: denormalized snapshot of what
50
+ // `discover()` returned, persisted locally so the thin-content
51
+ // synthesizer and read service have a canonical source — NOT a search
52
+ // index round-trip.
53
+ projection: jsonb("projection").$type().notNull(),
54
+ projection_etag: text("projection_etag"),
55
+ projection_seen_at: timestamp("projection_seen_at", { withTimezone: true })
56
+ .notNull()
57
+ .defaultNow(),
58
+ created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
59
+ updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
60
+ }, (table) => [
61
+ // Voyant-side lookup: one entity per (module, id).
62
+ uniqueIndex("catalog_sourced_entries_entity_uniq").on(table.entity_module, table.entity_id),
63
+ // Upstream-side idempotency: one row per (kind, connection, source_ref).
64
+ // Conditional partial-uniqueness skipped — discover() should always
65
+ // emit all three components for a sourced row, and the entity-uniq
66
+ // covers the rare case where source_ref is null.
67
+ uniqueIndex("catalog_sourced_entries_source_uniq").on(table.source_kind, table.source_connection_id, table.source_ref),
68
+ // Per-vertical × source listings.
69
+ index("catalog_sourced_entries_module_kind_idx").on(table.entity_module, table.source_kind),
70
+ // Withdrawal sweepers — find rows that haven't been seen in N days.
71
+ index("catalog_sourced_entries_status_seen_idx").on(table.status, table.last_seen_at),
72
+ // Per-connection age — useful for "what hasn't this connection
73
+ // refreshed lately" diagnostics.
74
+ index("catalog_sourced_entries_connection_age_idx").on(table.source_kind, table.source_connection_id, table.last_sourced_at),
75
+ ]);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Aggregated drizzle table exports for the catalog plane.
3
+ *
4
+ * Templates point their `drizzle.config.ts` at this single module to pick
5
+ * up every catalog-plane table in one go. Keeps the template config small
6
+ * and avoids the template having to track which catalog-plane sub-paths
7
+ * carry tables.
8
+ *
9
+ * Tables exported:
10
+ * - `catalogOverlayTable` — editorial overrides (§5.2)
11
+ * - `bookingCatalogSnapshotTable` — frozen booking snapshots (§5.3)
12
+ * - `catalogQuotesTable` — booking-engine quote records
13
+ * - `catalogSourcedEntriesTable` — durable sourced-entry store
14
+ * (sourced-content §2.5)
15
+ */
16
+ export { bookingDraftsTable, type InsertBookingDraft, type SelectBookingDraft, } from "./booking-engine/drafts-schema.js";
17
+ export { catalogQuotesTable, type InsertCatalogQuote, type SelectCatalogQuote, } from "./booking-engine/schema.js";
18
+ export { catalogOverlayTable, type InsertCatalogOverlay, OVERLAY_DEFAULT_SCOPE, type OverlayOrigin, type SelectCatalogOverlay, } from "./overlay/schema.js";
19
+ export { catalogSourcedEntriesTable, type InsertCatalogSourcedEntry, type SelectCatalogSourcedEntry, type SourcedEntryStatus, } from "./schema-sourced-entries.js";
20
+ export { bookingCatalogSnapshotTable, type InsertBookingCatalogSnapshot, type PricingBasis, readPricingBasis, type SelectBookingCatalogSnapshot, } from "./snapshot/schema.js";
21
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACL,mBAAmB,EACnB,KAAK,oBAAoB,EACzB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,0BAA0B,EAC1B,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,GACxB,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,2BAA2B,EAC3B,KAAK,4BAA4B,EACjC,KAAK,YAAY,EACjB,gBAAgB,EAChB,KAAK,4BAA4B,GAClC,MAAM,sBAAsB,CAAA"}
package/dist/schema.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Aggregated drizzle table exports for the catalog plane.
3
+ *
4
+ * Templates point their `drizzle.config.ts` at this single module to pick
5
+ * up every catalog-plane table in one go. Keeps the template config small
6
+ * and avoids the template having to track which catalog-plane sub-paths
7
+ * carry tables.
8
+ *
9
+ * Tables exported:
10
+ * - `catalogOverlayTable` — editorial overrides (§5.2)
11
+ * - `bookingCatalogSnapshotTable` — frozen booking snapshots (§5.3)
12
+ * - `catalogQuotesTable` — booking-engine quote records
13
+ * - `catalogSourcedEntriesTable` — durable sourced-entry store
14
+ * (sourced-content §2.5)
15
+ */
16
+ export { bookingDraftsTable, } from "./booking-engine/drafts-schema.js";
17
+ export { catalogQuotesTable, } from "./booking-engine/schema.js";
18
+ export { catalogOverlayTable, OVERLAY_DEFAULT_SCOPE, } from "./overlay/schema.js";
19
+ export { catalogSourcedEntriesTable, } from "./schema-sourced-entries.js";
20
+ export { bookingCatalogSnapshotTable, readPricingBasis, } from "./snapshot/schema.js";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cross-audience federated search.
3
+ *
4
+ * Per architecture §7, vectors are strictly per-audience — customer
5
+ * embedding pools only contain customer-visible text, partner pools only
6
+ * contain partner-visible text, etc. This means the most common admin AI
7
+ * use case ("find products similar to *X*" where *X* is in customer-
8
+ * facing language) needs to query a non-staff audience pool.
9
+ *
10
+ * Staff actors are authorized to query any audience pool; customer /
11
+ * partner / supplier agents are pinned to their own audience by API
12
+ * authorization. This helper takes a list of `search_audiences` and:
13
+ *
14
+ * 1. Verifies the actor is authorized for each requested audience.
15
+ * 2. Issues parallel `IndexerAdapter.search` calls — one per audience.
16
+ * 3. Deduplicates hits by entity id (same entity may rank in multiple
17
+ * pools; keep the highest-scoring instance).
18
+ * 4. Merges the per-pool result sets into a single ranked list.
19
+ *
20
+ * If the adapter declares `supportsCrossAudienceFederation`, the helper
21
+ * delegates to a single multi-collection adapter call instead of fanning
22
+ * out client-side. Either way the API contract is the same to callers.
23
+ *
24
+ * See `docs/architecture/catalog-architecture.md` for the design.
25
+ */
26
+ import type { Visibility } from "../contract.js";
27
+ import type { IndexerAdapter, SearchRequest, SearchResults } from "../indexer/contract.js";
28
+ export interface FederatedSearchOptions {
29
+ adapter: IndexerAdapter;
30
+ /**
31
+ * The actor making the request. The federation helper enforces:
32
+ * - `customer` / `partner` / `supplier` actors → may search only
33
+ * their own audience pool (no federation).
34
+ * - `staff` actors → may search any combination of audience pools.
35
+ */
36
+ actor: Visibility;
37
+ /** The audience pools to federate across. Must be a subset of allowed pools per actor. */
38
+ searchAudiences: Visibility[];
39
+ /** The vertical (entity_module) to search. */
40
+ vertical: string;
41
+ /** Locale + market for every slice. */
42
+ locale: string;
43
+ market: string;
44
+ /** The base search request — same shape passed to a single-slice search. */
45
+ request: SearchRequest;
46
+ }
47
+ /**
48
+ * Federate a search across multiple audience pools. Returns a unified
49
+ * `SearchResults` with deduplicated hits ranked by score.
50
+ */
51
+ export declare function federateAudienceSearch(options: FederatedSearchOptions): Promise<SearchResults>;
52
+ /**
53
+ * Merge several `SearchResults` into one, deduplicating by hit id and
54
+ * keeping the highest-scoring instance. Total is the count of unique ids
55
+ * across all pools (after dedupe).
56
+ */
57
+ export declare function mergeAndDedupe(perSlice: ReadonlyArray<SearchResults>, limit?: number): SearchResults;
58
+ //# sourceMappingURL=federate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federate.d.ts","sourceRoot":"","sources":["../../src/search/federate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EACV,cAAc,EAGd,aAAa,EACb,aAAa,EACd,MAAM,wBAAwB,CAAA;AAE/B,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAA;IACvB;;;;;OAKG;IACH,KAAK,EAAE,UAAU,CAAA;IACjB,0FAA0F;IAC1F,eAAe,EAAE,UAAU,EAAE,CAAA;IAC7B,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAA;IAChB,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,OAAO,EAAE,aAAa,CAAA;CACvB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,aAAa,CAAC,CAqCxB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,EACtC,KAAK,CAAC,EAAE,MAAM,GACb,aAAa,CAsBf"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Cross-audience federated search.
3
+ *
4
+ * Per architecture §7, vectors are strictly per-audience — customer
5
+ * embedding pools only contain customer-visible text, partner pools only
6
+ * contain partner-visible text, etc. This means the most common admin AI
7
+ * use case ("find products similar to *X*" where *X* is in customer-
8
+ * facing language) needs to query a non-staff audience pool.
9
+ *
10
+ * Staff actors are authorized to query any audience pool; customer /
11
+ * partner / supplier agents are pinned to their own audience by API
12
+ * authorization. This helper takes a list of `search_audiences` and:
13
+ *
14
+ * 1. Verifies the actor is authorized for each requested audience.
15
+ * 2. Issues parallel `IndexerAdapter.search` calls — one per audience.
16
+ * 3. Deduplicates hits by entity id (same entity may rank in multiple
17
+ * pools; keep the highest-scoring instance).
18
+ * 4. Merges the per-pool result sets into a single ranked list.
19
+ *
20
+ * If the adapter declares `supportsCrossAudienceFederation`, the helper
21
+ * delegates to a single multi-collection adapter call instead of fanning
22
+ * out client-side. Either way the API contract is the same to callers.
23
+ *
24
+ * See `docs/architecture/catalog-architecture.md` for the design.
25
+ */
26
+ /**
27
+ * Federate a search across multiple audience pools. Returns a unified
28
+ * `SearchResults` with deduplicated hits ranked by score.
29
+ */
30
+ export async function federateAudienceSearch(options) {
31
+ const { adapter, actor, searchAudiences, vertical, locale, market, request } = options;
32
+ if (searchAudiences.length === 0) {
33
+ throw new Error("federateAudienceSearch requires at least one searchAudience");
34
+ }
35
+ enforceAudienceAuthorization(actor, searchAudiences);
36
+ // If a single audience is requested, no federation needed — direct search.
37
+ if (searchAudiences.length === 1) {
38
+ const audience = searchAudiences[0];
39
+ const slice = { vertical, locale, audience, market };
40
+ return adapter.search(slice, request);
41
+ }
42
+ // Engine-side federation when supported — single adapter call.
43
+ if (adapter.capabilities.supportsCrossAudienceFederation) {
44
+ // The adapter contract doesn't currently expose multi-slice search
45
+ // directly. For Phase 2 v1 we use the client-side fan-out path below
46
+ // even when the engine could do better. A follow-up adds a
47
+ // `searchFederated(slices: IndexerSlice[], request)` method to the
48
+ // contract to take advantage of native federation.
49
+ // For now, fall through to the client-side path.
50
+ }
51
+ // Client-side fan-out: one search per audience pool, parallelized.
52
+ const slices = searchAudiences.map((audience) => ({
53
+ vertical,
54
+ locale,
55
+ audience,
56
+ market,
57
+ }));
58
+ const perSliceResults = await Promise.all(slices.map((slice) => adapter.search(slice, request)));
59
+ return mergeAndDedupe(perSliceResults, request.pagination?.limit);
60
+ }
61
+ /**
62
+ * Merge several `SearchResults` into one, deduplicating by hit id and
63
+ * keeping the highest-scoring instance. Total is the count of unique ids
64
+ * across all pools (after dedupe).
65
+ */
66
+ export function mergeAndDedupe(perSlice, limit) {
67
+ const byId = new Map();
68
+ for (const result of perSlice) {
69
+ for (const hit of result.hits) {
70
+ const existing = byId.get(hit.id);
71
+ if (!existing || hit.score > existing.score) {
72
+ byId.set(hit.id, hit);
73
+ }
74
+ }
75
+ }
76
+ // Sort by score descending — federated semantics rank by best score.
77
+ const merged = Array.from(byId.values()).sort((a, b) => b.score - a.score);
78
+ const limited = limit != null ? merged.slice(0, limit) : merged;
79
+ return {
80
+ hits: limited,
81
+ total: byId.size,
82
+ // Facets aren't merged — federation across audiences with different
83
+ // facet vocabularies is ambiguous. Callers that need facets should
84
+ // search a single audience.
85
+ };
86
+ }
87
+ /**
88
+ * Enforce per-actor authorization rules: customer / partner / supplier
89
+ * agents may only search their own audience pool. Staff actors may
90
+ * federate across any pools.
91
+ */
92
+ function enforceAudienceAuthorization(actor, requested) {
93
+ if (actor === "staff") {
94
+ // Staff may federate across anything.
95
+ return;
96
+ }
97
+ if (requested.length === 1 && requested[0] === actor) {
98
+ // Non-staff actor searching their own pool only — allowed.
99
+ return;
100
+ }
101
+ throw new Error(`Actor "${actor}" is not authorized to federate across audiences ${JSON.stringify(requested)}. ` +
102
+ `Non-staff actors may only search their own audience pool. To federate across audiences, the request must come from a staff actor.`);
103
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=federate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federate.test.d.ts","sourceRoot":"","sources":["../../src/search/federate.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { federateAudienceSearch, mergeAndDedupe } from "./federate.js";
3
+ const capabilities = {
4
+ supportsKeywordSearch: true,
5
+ supportsHybridSearch: true,
6
+ supportsVectorFields: true,
7
+ vectorDimensions: 1536,
8
+ maxVectorsPerDocument: null,
9
+ supportsCrossAudienceFederation: false,
10
+ supportsAdminDenormalization: true,
11
+ };
12
+ function hit(id, score) {
13
+ return { id, score, document: { id, fields: {} } };
14
+ }
15
+ function makeAdapter(perAudience) {
16
+ return {
17
+ capabilities,
18
+ async ensureCollection() { },
19
+ async upsert() { },
20
+ async delete() { },
21
+ async bulkReindex() { },
22
+ async search(slice) {
23
+ const hits = perAudience[slice.audience] ?? [];
24
+ return { hits, total: hits.length };
25
+ },
26
+ };
27
+ }
28
+ describe("mergeAndDedupe", () => {
29
+ it("keeps the highest-scoring instance when an entity appears in multiple pools", () => {
30
+ const merged = mergeAndDedupe([
31
+ { hits: [hit("a", 1), hit("b", 2)], total: 2 },
32
+ { hits: [hit("a", 5), hit("c", 3)], total: 2 },
33
+ ]);
34
+ const ids = merged.hits.map((h) => h.id);
35
+ expect(ids).toEqual(["a", "c", "b"]); // sorted by score desc
36
+ const aHit = merged.hits.find((h) => h.id === "a");
37
+ expect(aHit?.score).toBe(5);
38
+ expect(merged.total).toBe(3);
39
+ });
40
+ it("respects the limit parameter", () => {
41
+ const merged = mergeAndDedupe([{ hits: [hit("a", 1), hit("b", 2), hit("c", 3)], total: 3 }], 2);
42
+ expect(merged.hits).toHaveLength(2);
43
+ expect(merged.total).toBe(3); // total is unique-id count, not limited
44
+ });
45
+ it("returns an empty result when no pools have hits", () => {
46
+ const merged = mergeAndDedupe([]);
47
+ expect(merged.hits).toEqual([]);
48
+ expect(merged.total).toBe(0);
49
+ });
50
+ });
51
+ describe("federateAudienceSearch — authorization", () => {
52
+ it("rejects non-staff actors trying to federate beyond their own pool", async () => {
53
+ const adapter = makeAdapter({});
54
+ await expect(federateAudienceSearch({
55
+ adapter,
56
+ actor: "customer",
57
+ searchAudiences: ["customer", "partner"],
58
+ vertical: "products",
59
+ locale: "en-GB",
60
+ market: "default",
61
+ request: { query: "x", mode: "keyword" },
62
+ })).rejects.toThrow(/not authorized to federate/);
63
+ });
64
+ it("allows non-staff actors to search their own audience pool only", async () => {
65
+ const adapter = makeAdapter({ customer: [hit("a", 1)] });
66
+ const result = await federateAudienceSearch({
67
+ adapter,
68
+ actor: "customer",
69
+ searchAudiences: ["customer"],
70
+ vertical: "products",
71
+ locale: "en-GB",
72
+ market: "default",
73
+ request: { query: "x", mode: "keyword" },
74
+ });
75
+ expect(result.hits).toHaveLength(1);
76
+ });
77
+ it("rejects non-staff actors trying to search a different audience pool", async () => {
78
+ const adapter = makeAdapter({});
79
+ await expect(federateAudienceSearch({
80
+ adapter,
81
+ actor: "customer",
82
+ searchAudiences: ["partner"],
83
+ vertical: "products",
84
+ locale: "en-GB",
85
+ market: "default",
86
+ request: { query: "x", mode: "keyword" },
87
+ })).rejects.toThrow(/not authorized to federate/);
88
+ });
89
+ it("allows staff actors to federate across multiple audience pools", async () => {
90
+ const adapter = makeAdapter({
91
+ customer: [hit("a", 1)],
92
+ partner: [hit("b", 2)],
93
+ });
94
+ const result = await federateAudienceSearch({
95
+ adapter,
96
+ actor: "staff",
97
+ searchAudiences: ["customer", "partner"],
98
+ vertical: "products",
99
+ locale: "en-GB",
100
+ market: "default",
101
+ request: { query: "x", mode: "keyword" },
102
+ });
103
+ expect(result.hits.map((h) => h.id).sort()).toEqual(["a", "b"]);
104
+ expect(result.total).toBe(2);
105
+ });
106
+ });
107
+ describe("federateAudienceSearch — single audience shortcut", () => {
108
+ it("issues a single adapter call when only one audience is requested", async () => {
109
+ let callCount = 0;
110
+ const wrapped = {
111
+ capabilities,
112
+ async ensureCollection() { },
113
+ async upsert() { },
114
+ async delete() { },
115
+ async bulkReindex() { },
116
+ async search(slice) {
117
+ callCount++;
118
+ return { hits: [hit(`from-${slice.audience}`, 1)], total: 1 };
119
+ },
120
+ };
121
+ await federateAudienceSearch({
122
+ adapter: wrapped,
123
+ actor: "staff",
124
+ searchAudiences: ["customer"],
125
+ vertical: "products",
126
+ locale: "en-GB",
127
+ market: "default",
128
+ request: { query: "x", mode: "keyword" },
129
+ });
130
+ expect(callCount).toBe(1);
131
+ });
132
+ });
133
+ describe("federateAudienceSearch — empty input rejected", () => {
134
+ it("requires at least one searchAudience", async () => {
135
+ const adapter = makeAdapter({});
136
+ await expect(federateAudienceSearch({
137
+ adapter,
138
+ actor: "staff",
139
+ searchAudiences: [],
140
+ vertical: "products",
141
+ locale: "en-GB",
142
+ market: "default",
143
+ request: { query: "x", mode: "keyword" },
144
+ })).rejects.toThrow(/at least one searchAudience/);
145
+ });
146
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tier 2 two-stage-search rerank helper for browse-time pricing.
3
+ *
4
+ * Storefront BFFs use this for "refresh prices for these dates" actions and
5
+ * other high-intent queries where Tier 1 indexed price summaries are too
6
+ * approximate. The pattern: indexer returns top-N candidates ranked by
7
+ * cached price; this helper calls the source adapter's live-pricing
8
+ * operation for those candidates with the customer's exact dates /
9
+ * occupancy / market / currency, then resorts.
10
+ *
11
+ * v1 ships the helper but storefronts opt in per query — never always-on.
12
+ *
13
+ * See `docs/architecture/catalog-architecture.md` §5.4.3 Tier 2 for design.
14
+ */
15
+ import type { SearchHit, SearchResults } from "../indexer/contract.js";
16
+ /**
17
+ * The live-pricing function the storefront BFF supplies. Receives a batch
18
+ * of candidate ids (typically top-N from the indexer) plus the rerank
19
+ * parameters (dates / occupancy / market / currency); returns a per-id
20
+ * price map.
21
+ *
22
+ * Implementations route per-id calls to the right source adapter — the BFF
23
+ * knows which adapter owns each entity from the catalog plane's provenance.
24
+ */
25
+ export type LivePriceFn = (ids: string[], parameters: RerankParameters) => Promise<Map<string, LivePriceResult>>;
26
+ /** Parameters that scope the live-pricing rerank. Shape is vertical-agnostic. */
27
+ export interface RerankParameters {
28
+ dates?: {
29
+ start: string;
30
+ end: string;
31
+ };
32
+ occupancy?: Record<string, number>;
33
+ market: string;
34
+ currency: string;
35
+ /** Per-source timeout in milliseconds. Defaults vary by storefront. */
36
+ perSourceTimeoutMs?: number;
37
+ }
38
+ /**
39
+ * Result of a single live-pricing call. The price source is captured so
40
+ * downstream consumers can mark stale fallbacks.
41
+ */
42
+ export interface LivePriceResult {
43
+ amount: number;
44
+ currency: string;
45
+ source: "live" | "stale";
46
+ }
47
+ /** Reranked hit with the price provenance attached. */
48
+ export interface RerankedHit extends SearchHit {
49
+ /** The live-resolved price; may be the indexed fallback if live timed out. */
50
+ rerankedPrice?: LivePriceResult;
51
+ }
52
+ export interface RerankOptions {
53
+ /**
54
+ * Top-N to pass through the live-pricing path. Default 100; bounded so the
55
+ * rerank cost stays predictable.
56
+ */
57
+ topN?: number;
58
+ /**
59
+ * If true, hits without a successful live price drop to the bottom of the
60
+ * sorted result. If false (default), they keep their indexer-ranked order
61
+ * but carry a `priceSource: "stale"` marker in `rerankedPrice`.
62
+ */
63
+ dropOnLiveMiss?: boolean;
64
+ }
65
+ /**
66
+ * Reranks indexer results against live source pricing. The hit list is
67
+ * narrowed to `topN`, live-priced via the supplied function, resorted by
68
+ * live price, and merged back with the un-reranked tail (which keeps its
69
+ * indexer ranking).
70
+ *
71
+ * Failures from the live function are caught individually — one timing-out
72
+ * source does not tank the rerank.
73
+ */
74
+ export declare function rerank(results: SearchResults, livePrice: LivePriceFn, parameters: RerankParameters, options?: RerankOptions): Promise<SearchResults & {
75
+ hits: RerankedHit[];
76
+ }>;
77
+ //# sourceMappingURL=rerank.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rerank.d.ts","sourceRoot":"","sources":["../../src/search/rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAEtE;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,MAAM,EAAE,EACb,UAAU,EAAE,gBAAgB,KACzB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAA;AAE1C,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,GAAG,OAAO,CAAA;CACzB;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,8EAA8E;IAC9E,aAAa,CAAC,EAAE,eAAe,CAAA;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,WAAW,EACtB,UAAU,EAAE,gBAAgB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,aAAa,GAAG;IAAE,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,CAAC,CA8ClD"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Tier 2 two-stage-search rerank helper for browse-time pricing.
3
+ *
4
+ * Storefront BFFs use this for "refresh prices for these dates" actions and
5
+ * other high-intent queries where Tier 1 indexed price summaries are too
6
+ * approximate. The pattern: indexer returns top-N candidates ranked by
7
+ * cached price; this helper calls the source adapter's live-pricing
8
+ * operation for those candidates with the customer's exact dates /
9
+ * occupancy / market / currency, then resorts.
10
+ *
11
+ * v1 ships the helper but storefronts opt in per query — never always-on.
12
+ *
13
+ * See `docs/architecture/catalog-architecture.md` §5.4.3 Tier 2 for design.
14
+ */
15
+ /**
16
+ * Reranks indexer results against live source pricing. The hit list is
17
+ * narrowed to `topN`, live-priced via the supplied function, resorted by
18
+ * live price, and merged back with the un-reranked tail (which keeps its
19
+ * indexer ranking).
20
+ *
21
+ * Failures from the live function are caught individually — one timing-out
22
+ * source does not tank the rerank.
23
+ */
24
+ export async function rerank(results, livePrice, parameters, options = {}) {
25
+ const topN = options.topN ?? 100;
26
+ const head = results.hits.slice(0, topN);
27
+ const tail = results.hits.slice(topN);
28
+ const headIds = head.map((h) => h.id);
29
+ let priceMap;
30
+ try {
31
+ priceMap = await livePrice(headIds, parameters);
32
+ }
33
+ catch {
34
+ // Live call failed entirely. Return the indexer-ranked results unchanged.
35
+ return { ...results, hits: head.concat(tail) };
36
+ }
37
+ // Attach prices to hits.
38
+ const reranked = head.map((hit) => {
39
+ const price = priceMap.get(hit.id);
40
+ return price ? { ...hit, rerankedPrice: price } : { ...hit };
41
+ });
42
+ // Filter / order:
43
+ // - hits with live prices sort by price ascending
44
+ // - hits with stale fallback sort after the live ones (stable order)
45
+ // - hits with no price are dropped or kept based on dropOnLiveMiss
46
+ const live = [];
47
+ const stale = [];
48
+ const missing = [];
49
+ for (const hit of reranked) {
50
+ if (!hit.rerankedPrice) {
51
+ missing.push(hit);
52
+ }
53
+ else if (hit.rerankedPrice.source === "live") {
54
+ live.push(hit);
55
+ }
56
+ else {
57
+ stale.push(hit);
58
+ }
59
+ }
60
+ live.sort((a, b) => a.rerankedPrice.amount - b.rerankedPrice.amount);
61
+ const headOut = options.dropOnLiveMiss
62
+ ? [...live, ...stale]
63
+ : [...live, ...stale, ...missing];
64
+ return {
65
+ ...results,
66
+ hits: [...headOut, ...tail],
67
+ };
68
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rerank.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rerank.test.d.ts","sourceRoot":"","sources":["../../src/search/rerank.test.ts"],"names":[],"mappings":""}