var __defProp = Object.defineProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// src/toolbox/search/index.ts
var search_exports = {};
__export(search_exports, {
  Search: () => Search,
  builtin: () => builtin2,
  createSearch: () => createSearch,
  learnRankingWeights: () => learnRankingWeights
});

// src/toolbox/common/types.ts
var identityAdapter = { fromRow: (d) => d };
function nfkc(s) {
  return s.normalize("NFKC");
}
function clamp(n, lo, hi) {
  return Math.max(lo, Math.min(hi, n));
}
function combine(kind, scores, weights) {
  if (typeof kind === "function") return kind(scores, weights);
  const ids = Object.keys(scores);
  if (kind === "product") {
    let p = 1;
    for (const id of ids) {
      const w = weights[id] ?? 1;
      p *= Math.pow(Math.max(0, scores[id] ?? 0), w);
    }
    return p;
  }
  if (kind === "log_sum") {
    let s2 = 0;
    for (const id of ids) {
      const w = weights[id] ?? 1;
      s2 += w * Math.log(Math.max(1e-9, scores[id] ?? 0));
    }
    return Math.exp(s2);
  }
  let s = 0;
  for (const id of ids) {
    const w = weights[id] ?? 1;
    s += w * (scores[id] ?? 0);
  }
  return s;
}

// src/utils/storage/memory.ts
function memoryAdapter() {
  const store = /* @__PURE__ */ new Map();
  return {
    async open() {
    },
    async get(key) {
      return store.get(key);
    },
    async set(key, value) {
      store.set(key, value);
    },
    async delete(key) {
      store.delete(key);
    },
    async *iterate(prefix) {
      for (const [k, v] of store) if (k.startsWith(prefix)) yield [k, v];
    },
    async close() {
      store.clear();
    }
  };
}

// src/utils/storage/local-storage.ts
function localStorageAdapter(prefix = "dddk:") {
  const fullKey = (k) => prefix + k;
  return {
    async open() {
      if (typeof localStorage === "undefined") {
        throw new Error("localStorageAdapter: localStorage is unavailable in this runtime");
      }
    },
    async get(key) {
      const raw = localStorage.getItem(fullKey(key));
      if (raw === null) return void 0;
      try {
        return JSON.parse(raw);
      } catch {
        return raw;
      }
    },
    async set(key, value) {
      localStorage.setItem(fullKey(key), JSON.stringify(value));
    },
    async delete(key) {
      localStorage.removeItem(fullKey(key));
    },
    async *iterate(p) {
      const full = prefix + p;
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (!k || !k.startsWith(full)) continue;
        const raw = localStorage.getItem(k);
        if (raw === null) continue;
        let val;
        try {
          val = JSON.parse(raw);
        } catch {
          val = raw;
        }
        yield [k.slice(prefix.length), val];
      }
    },
    async close() {
    }
  };
}

// src/utils/storage/indexeddb.ts
var STORE = "kv";
function promisify(req) {
  return new Promise((resolve, reject) => {
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}
function indexedDBAdapter(dbName = "dddk-toolbox") {
  let db;
  return {
    async open() {
      if (typeof indexedDB === "undefined") {
        throw new Error("indexedDBAdapter: IndexedDB unavailable; falling back recommended for SSR / Node");
      }
      db = await new Promise((resolve, reject) => {
        const req = indexedDB.open(dbName, 1);
        req.onupgradeneeded = () => {
          const d = req.result;
          if (!d.objectStoreNames.contains(STORE)) d.createObjectStore(STORE);
        };
        req.onsuccess = () => resolve(req.result);
        req.onerror = () => reject(req.error);
      });
    },
    async get(key) {
      if (!db) throw new Error("indexedDBAdapter: not opened");
      const tx = db.transaction(STORE, "readonly");
      return await promisify(tx.objectStore(STORE).get(key));
    },
    async set(key, value) {
      if (!db) throw new Error("indexedDBAdapter: not opened");
      const tx = db.transaction(STORE, "readwrite");
      await promisify(tx.objectStore(STORE).put(value, key));
    },
    async delete(key) {
      if (!db) throw new Error("indexedDBAdapter: not opened");
      const tx = db.transaction(STORE, "readwrite");
      await promisify(tx.objectStore(STORE).delete(key));
    },
    async *iterate(prefix) {
      if (!db) throw new Error("indexedDBAdapter: not opened");
      const tx = db.transaction(STORE, "readonly");
      const store = tx.objectStore(STORE);
      const range = IDBKeyRange.bound(prefix, prefix + "\uFFFF");
      const cursorReq = store.openCursor(range);
      while (true) {
        const cur = await new Promise((resolve, reject) => {
          cursorReq.onsuccess = () => resolve(cursorReq.result);
          cursorReq.onerror = () => reject(cursorReq.error);
        });
        if (!cur) break;
        yield [String(cur.key), cur.value];
        cur.continue();
      }
    },
    async close() {
      db?.close();
      db = void 0;
    }
  };
}

// src/utils/storage/index.ts
function resolveStorage(kind) {
  if (!kind || kind === "memory") return memoryAdapter();
  if (kind === "localstorage") return localStorageAdapter();
  if (kind === "indexeddb") {
    if (typeof indexedDB === "undefined") return memoryAdapter();
    return indexedDBAdapter();
  }
  return kind;
}

// src/utils/text/tokenize.ts
var CJK_RE = /[぀-ヿ㐀-䶿一-鿿가-힯]/;
var SE_ASIAN_RE = /[฀-๿຀-໿ក-៿က-႟]/;
var LETTER_NUM_RE = /[\p{L}\p{N}]/u;
function nfkc2(s) {
  return s.normalize("NFKC");
}
function isNoSpaceScript(text) {
  const sample = text.slice(0, 200);
  return CJK_RE.test(sample) || SE_ASIAN_RE.test(sample);
}
function tokenizeNoSpace(text) {
  const out = [];
  for (const c of nfkc2(text)) {
    if (LETTER_NUM_RE.test(c)) out.push(c.toLowerCase());
  }
  return out;
}
function tokenizeWhitespace(text) {
  return nfkc2(text).toLowerCase().split(/[\s\p{P}\p{S}]+/u).filter((t) => t.length > 0);
}
function extractFeatures(text) {
  const noSpace = isNoSpaceScript(text);
  const tokens = noSpace ? tokenizeNoSpace(text) : tokenizeWhitespace(text);
  const bigrams = [];
  const sep = noSpace ? "" : "_";
  for (let i = 0; i + 1 < tokens.length; i++) {
    bigrams.push(tokens[i] + sep + tokens[i + 1]);
  }
  return [...tokens, ...bigrams];
}

// src/utils/text/bm25.ts
function bm25TermScore(tf, df, totalDocs, docLen, avgDocLen, opts = {}) {
  const k1 = opts.k1 ?? 1.5;
  const b = opts.b ?? 0.75;
  if (tf <= 0 || df <= 0 || totalDocs <= 0) return 0;
  const idf = Math.log((totalDocs - df + 0.5) / (df + 0.5) + 1);
  const denom = tf + k1 * (1 - b + b * docLen / Math.max(1, avgDocLen));
  return idf * (tf * (k1 + 1)) / denom;
}
function saturateTf(rawTf, cap) {
  if (cap <= 0) return rawTf;
  return cap * (1 - Math.exp(-rawTf / cap));
}

// src/toolbox/search/store.ts
var SearchStore = class {
  postings = /* @__PURE__ */ new Map();
  docs = /* @__PURE__ */ new Map();
  fieldStats = /* @__PURE__ */ new Map();
  totalDocs = 0;
  features = /* @__PURE__ */ new Map();
  // docId → unique features (for similarTo / removal)
  add(doc, extract) {
    if (this.docs.has(doc.id)) this.remove(doc.id);
    this.docs.set(doc.id, doc);
    this.totalDocs++;
    const fieldFeats = /* @__PURE__ */ new Map();
    for (const [field, value] of Object.entries(doc.fields)) {
      const str = String(value ?? "");
      const feats = extract(str);
      fieldFeats.set(field, feats);
      const stat = this.fieldStats.get(field) ?? { totalLength: 0, docCount: 0 };
      stat.totalLength += feats.length;
      stat.docCount++;
      this.fieldStats.set(field, stat);
    }
    const featCounts = /* @__PURE__ */ new Map();
    for (const [field, feats] of fieldFeats) {
      for (const f of feats) {
        let m = featCounts.get(f);
        if (!m) {
          m = {};
          featCounts.set(f, m);
        }
        m[field] = (m[field] ?? 0) + 1;
      }
    }
    const uniqueFeats = [];
    for (const [f, fieldFreqs] of featCounts) {
      uniqueFeats.push(f);
      let list = this.postings.get(f);
      if (!list) {
        list = [];
        this.postings.set(f, list);
      }
      list.push({ docId: doc.id, fieldFreqs });
    }
    this.features.set(doc.id, uniqueFeats);
  }
  remove(docId) {
    const doc = this.docs.get(docId);
    if (!doc) return;
    const feats = this.features.get(docId) ?? [];
    for (const f of feats) {
      const list = this.postings.get(f);
      if (!list) continue;
      const next = list.filter((p) => p.docId !== docId);
      if (next.length === 0) this.postings.delete(f);
      else this.postings.set(f, next);
    }
    for (const [field, value] of Object.entries(doc.fields)) {
      const stat = this.fieldStats.get(field);
      if (!stat) continue;
      const guessLen = this.features.get(docId)?.length ?? 0;
      stat.totalLength = Math.max(0, stat.totalLength - guessLen);
      stat.docCount = Math.max(0, stat.docCount - 1);
      void value;
    }
    this.features.delete(docId);
    this.docs.delete(docId);
    this.totalDocs = Math.max(0, this.totalDocs - 1);
  }
  candidates(queryFeats) {
    const set = /* @__PURE__ */ new Set();
    for (const f of queryFeats) {
      const list = this.postings.get(f);
      if (!list) continue;
      for (const p of list) set.add(p.docId);
    }
    return set;
  }
  postingsFor(feature) {
    return this.postings.get(feature);
  }
  avgFieldLen(field) {
    const s = this.fieldStats.get(field);
    if (!s || s.docCount === 0) return 1;
    return s.totalLength / s.docCount;
  }
};

// src/toolbox/search/builtin.ts
function bm25Field(opts) {
  return {
    id: "bm25_field",
    compute(doc, ctx) {
      const s = ctx._store;
      if (!s) return 0;
      const uniqQ = new Set(ctx.queryFeatures);
      let total = 0;
      for (const f of uniqQ) {
        const postings = s.postingsFor(f);
        if (!postings) continue;
        const hit = postings.find((p) => p.docId === doc.id);
        if (!hit) continue;
        const df = postings.length;
        for (const [field, freq] of Object.entries(hit.fieldFreqs)) {
          const fw = opts.weights[field];
          if (!fw) continue;
          const sat = saturateTf(freq, fw.saturation ?? 7);
          const avgLen = s.avgFieldLen(field);
          const docLen = avgLen;
          const score = bm25TermScore(sat, df, s.totalDocs, docLen, avgLen, opts);
          total += score * fw.weight;
        }
      }
      return total;
    }
  };
}
function recencyDecay(opts) {
  const half = (opts.halfLifeDays ?? 90) * 864e5;
  return {
    id: "recency_decay",
    compute(doc, ctx) {
      const ts = Number(doc.meta?.[opts.field] ?? 0);
      if (!ts) return 1;
      const age = Math.max(0, ctx.now - ts);
      return Math.pow(0.5, age / half);
    }
  };
}
function newProductBoost(opts) {
  const window = (opts.withinDays ?? 30) * 864e5;
  const m = opts.multiplier ?? 1.4;
  return {
    id: "new_product_boost",
    compute(doc, ctx) {
      const ts = Number(doc.meta?.[opts.field] ?? 0);
      if (!ts) return 1;
      return ctx.now - ts < window ? m : 1;
    }
  };
}
function bestsellerBoost(opts) {
  const max = opts.maxBoost ?? 1.3;
  return {
    id: "bestseller_boost",
    compute(doc) {
      const v = Number(doc.meta?.[opts.field] ?? 0);
      if (v <= 0) return 1;
      if (opts.method === "rank_inverse") {
        return 1 + (max - 1) / Math.max(1, v);
      }
      const norm = Math.log1p(v) / Math.log1p(1e4);
      return 1 + (max - 1) * Math.min(1, norm);
    }
  };
}
function promoBoost(opts = {}) {
  const field = opts.field ?? "promo_boost";
  return {
    id: "promo_boost",
    compute(doc) {
      const v = Number(doc.meta?.[field] ?? 1);
      return v > 0 ? v : 1;
    }
  };
}
function inventoryFilter(opts = {}) {
  const field = opts.field ?? "stock_qty";
  const min = opts.min ?? 1;
  return {
    id: "inventory_filter",
    compute: () => 1,
    gate(doc) {
      const v = Number(doc.meta?.[field] ?? Infinity);
      return v >= min;
    }
  };
}
function activeFilter(opts = {}) {
  const field = opts.field ?? "active";
  return {
    id: "active_filter",
    compute: () => 1,
    gate(doc) {
      const v = doc.meta?.[field];
      return v === void 0 || Boolean(v);
    }
  };
}
var builtin = {
  bm25Field,
  recencyDecay,
  newProductBoost,
  bestsellerBoost,
  promoBoost,
  inventoryFilter,
  activeFilter
};

// src/toolbox/search/learn.ts
function sigmoid(x) {
  return 1 / (1 + Math.exp(-x));
}
function learnRankingWeights(events, opts) {
  const fields = opts.fields;
  const lr = opts.learningRate ?? 0.05;
  const l2 = opts.l2 ?? 1e-4;
  const iterations = opts.iterations ?? 200;
  const batchSize = opts.batchSize ?? 32;
  const pairs = [];
  for (const ev of events) {
    const clicked = ev.shown.find((s) => s.docId === ev.clickedDocId);
    if (!clicked) continue;
    for (const s of ev.shown) {
      if (s.docId === ev.clickedDocId) continue;
      const delta = {};
      for (const f of fields) {
        delta[f] = (clicked.fieldScores[f] ?? 0) - (s.fieldScores[f] ?? 0);
      }
      pairs.push({ delta });
    }
  }
  const weights = {};
  for (const f of fields) weights[f] = 1;
  let lastLoss = Infinity;
  for (let iter = 0; iter < iterations; iter++) {
    for (let i = pairs.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [pairs[i], pairs[j]] = [pairs[j], pairs[i]];
    }
    let totalLoss = 0;
    for (let bs = 0; bs < pairs.length; bs += batchSize) {
      const batch = pairs.slice(bs, bs + batchSize);
      const grads = {};
      for (const f of fields) grads[f] = 0;
      let batchLoss = 0;
      for (const p of batch) {
        let z = 0;
        for (const f of fields) z += (weights[f] ?? 0) * (p.delta[f] ?? 0);
        const prob = sigmoid(z);
        batchLoss += -Math.log(Math.max(1e-12, prob));
        const err = 1 - prob;
        for (const f of fields) {
          grads[f] = (grads[f] ?? 0) + err * (p.delta[f] ?? 0);
        }
      }
      for (const f of fields) {
        const g = (grads[f] ?? 0) / Math.max(1, batch.length);
        weights[f] = (weights[f] ?? 0) + lr * g - lr * l2 * (weights[f] ?? 0);
      }
      totalLoss += batchLoss;
    }
    lastLoss = totalLoss / Math.max(1, pairs.length);
  }
  return { weights, loss: lastLoss, iterations };
}

// src/toolbox/search/index.ts
var builtin2 = builtin;
var SNAPSHOT_KEY = (id) => `search:${id}:snapshot`;
var VERSION_KEY = (id) => `search:${id}:version`;
var Search = class {
  id;
  store = new SearchStore();
  adapter;
  storage;
  scorers;
  weights;
  extract;
  combiner;
  topK;
  scoreThreshold;
  opts;
  opened = false;
  lowConfHook;
  pollTimer;
  unsubscribe;
  constructor(opts) {
    this.id = opts.id ?? "default";
    this.adapter = opts.adapter ?? identityAdapter;
    this.storage = resolveStorage(opts.storage);
    this.scorers = opts.scorers ?? [];
    this.weights = opts.weights ?? {};
    this.extract = opts.extractFeatures ?? extractFeatures;
    this.combiner = opts.combiner ?? "product";
    this.topK = opts.topK ?? 20;
    this.scoreThreshold = opts.scoreThreshold ?? 0;
    this.opts = opts;
  }
  async init() {
    await this.storage.open(`dddk-search-${this.id}`);
    this.opened = true;
    const snapshot = await this.storage.get(SNAPSHOT_KEY(this.id));
    if (snapshot?.docs) {
      for (const d of snapshot.docs) this.store.add(d, this.extract);
    }
    if (this.opts.sync) await this.startSync();
  }
  async addDoc(row) {
    const doc = this.adapter.fromRow(row);
    this.store.add(doc, this.extract);
    await this.persist();
  }
  async addDocs(rows) {
    for (const row of rows) {
      const doc = this.adapter.fromRow(row);
      this.store.add(doc, this.extract);
    }
    await this.persist();
  }
  async removeDoc(id) {
    this.store.remove(id);
    await this.persist();
  }
  async updateDoc(row) {
    return this.addDoc(row);
  }
  async query(text, opts = {}) {
    const queryFeatures = this.extract(text);
    const ctx = {
      query: text,
      queryFeatures,
      now: Date.now(),
      customMeta: opts.customMeta,
      _store: this.store
    };
    const candIds = this.store.candidates(queryFeatures);
    const results = [];
    for (const id of candIds) {
      const doc = this.store.docs.get(id);
      if (!doc) continue;
      let passed = true;
      const partial = {};
      for (const s of this.scorers) {
        if (s.gate && !s.gate(doc, ctx)) {
          passed = false;
          break;
        }
        partial[s.id] = s.compute(doc, ctx);
      }
      if (!passed) continue;
      const score = combine(this.combiner ?? "product", partial, this.weights);
      if (score < (opts.scoreThreshold ?? this.scoreThreshold)) continue;
      const result = { doc, score };
      if (opts.explain) {
        result.explanation = {
          docId: id,
          total: score,
          contributions: Object.entries(partial).map(([signalId, value]) => ({
            signalId,
            value,
            weight: this.weights[signalId] ?? 1
          }))
        };
      }
      results.push(result);
    }
    results.sort((a, b) => b.score - a.score);
    const top = results.slice(0, opts.topK ?? this.topK);
    if (this.lowConfHook && top.length > 0 && (top[0]?.score ?? 0) < (opts.scoreThreshold ?? this.scoreThreshold) * 2) {
      return await this.lowConfHook(text, top);
    }
    return top;
  }
  explain(text, docId) {
    const queryFeatures = this.extract(text);
    const doc = this.store.docs.get(docId);
    if (!doc) return void 0;
    const ctx = { query: text, queryFeatures, now: Date.now(), _store: this.store };
    const partial = {};
    for (const s of this.scorers) {
      if (s.gate && !s.gate(doc, ctx)) return { docId, total: 0, gated: true, contributions: [] };
      partial[s.id] = s.compute(doc, ctx);
    }
    const total = combine(this.combiner ?? "product", partial, this.weights);
    return {
      docId,
      total,
      contributions: Object.entries(partial).map(([signalId, value]) => ({
        signalId,
        value,
        weight: this.weights[signalId] ?? 1
      }))
    };
  }
  onLowConfidence(fn) {
    this.lowConfHook = fn;
  }
  async snapshot() {
    return { docs: [...this.store.docs.values()] };
  }
  async restore(snap) {
    this.store = new SearchStore();
    for (const d of snap.docs) this.store.add(d, this.extract);
    await this.persist();
  }
  async dispose() {
    if (this.pollTimer) clearInterval(this.pollTimer);
    if (this.unsubscribe) this.unsubscribe();
    if (this.opened) await this.storage.close();
    this.opened = false;
  }
  getDoc(id) {
    return this.store.docs.get(id);
  }
  allDocs() {
    return this.store.docs.values();
  }
  getFeatures(id) {
    return this.store.features.get(id);
  }
  // Internal access used by adjacent modules (e.g. recommend.contentSimilarity)
  _store() {
    return this.store;
  }
  async persist() {
    if (!this.opened) return;
    await this.storage.set(SNAPSHOT_KEY(this.id), { docs: [...this.store.docs.values()] });
  }
  async startSync() {
    const sync = this.opts.sync;
    if (!sync) return;
    const currentVersion = await this.storage.get(VERSION_KEY(this.id));
    if (currentVersion === void 0 && sync.bootstrap) {
      const payload = await sync.bootstrap();
      for (const row of payload.rows) {
        const doc = this.adapter.fromRow(row);
        this.store.add(doc, this.extract);
      }
      await this.persist();
      await this.storage.set(VERSION_KEY(this.id), payload.version);
    }
    if (sync.subscribe) {
      this.unsubscribe = sync.subscribe(async (delta) => this.applyDeltaPayload(delta));
    } else if (sync.fetchDelta && sync.pollIntervalMs) {
      this.pollTimer = setInterval(async () => {
        const v = await this.storage.get(VERSION_KEY(this.id));
        if (v === void 0) return;
        try {
          const payload = await sync.fetchDelta(v);
          await this.applyDeltaPayload(payload);
        } catch {
        }
      }, sync.pollIntervalMs);
    }
  }
  async applyDeltaPayload(payload) {
    for (const ch of payload.changes) {
      if (ch.op === "remove" && ch.id) this.store.remove(ch.id);
      else if (ch.row) {
        const d = this.adapter.fromRow(ch.row);
        this.store.add(d, this.extract);
      }
    }
    await this.persist();
    await this.storage.set(VERSION_KEY(this.id), payload.version);
  }
};
function createSearch(opts) {
  return new Search(opts);
}

// src/toolbox/recommend/index.ts
var recommend_exports = {};
__export(recommend_exports, {
  Recommend: () => Recommend,
  builtin: () => builtin3,
  createRecommend: () => createRecommend
});
var Recommend = class {
  id;
  catalog;
  customerId;
  storage;
  signals;
  weights;
  combiner;
  topK;
  prefs = [];
  opened = false;
  opts;
  constructor(opts) {
    this.id = opts.id ?? "default";
    this.catalog = opts.catalog;
    this.customerId = opts.customerId;
    this.storage = resolveStorage(opts.storage ?? "indexeddb");
    this.signals = opts.signals;
    this.weights = opts.weights ?? {};
    this.combiner = opts.combiner ?? "product";
    this.topK = opts.topK ?? 10;
    this.opts = opts;
  }
  async init() {
    await this.storage.open(`dddk-recommend-${this.id}`);
    this.opened = true;
    const snap = await this.storage.get("recommend:prefs");
    if (snap) this.prefs = snap;
    else if (this.opts.prefsSync?.bootstrap) {
      this.prefs = await this.opts.prefsSync.bootstrap();
      await this.storage.set("recommend:prefs", this.prefs);
    }
  }
  async recordPreference(p) {
    const pref = { ...p, ts: p.ts ?? Date.now() };
    this.prefs.push(pref);
    if (this.opened) await this.storage.set("recommend:prefs", this.prefs);
    if (this.opts.prefsSync?.pushChange) {
      try {
        await this.opts.prefsSync.pushChange(pref);
      } catch {
      }
    }
  }
  async similarTo(productId, opts = {}) {
    const ctx = {
      userId: this.customerId,
      source: "similarTo",
      sourceProductId: productId,
      now: Date.now(),
      prefs: this.prefs
    };
    return this.rank(ctx, (d) => d.id !== productId, opts.topK ?? this.topK);
  }
  async forCustomer(opts = {}) {
    const ctx = {
      userId: this.customerId,
      source: "forCustomer",
      now: Date.now(),
      prefs: this.prefs
    };
    return this.rank(ctx, () => true, opts.topK ?? this.topK);
  }
  async goesWith(productIds, opts = {}) {
    const ctx = {
      userId: this.customerId,
      source: "goesWith",
      sourceProductId: productIds,
      now: Date.now(),
      prefs: this.prefs
    };
    const set = new Set(productIds);
    return this.rank(ctx, (d) => !set.has(d.id), opts.topK ?? this.topK);
  }
  explain(productId, ctx) {
    const doc = this.catalog.getDoc(productId);
    if (!doc) return void 0;
    const full = {
      userId: this.customerId,
      source: "forCustomer",
      now: Date.now(),
      prefs: this.prefs,
      ...ctx
    };
    const partial = {};
    for (const s of this.signals) {
      if (s.gate && !s.gate(doc, full)) return { docId: productId, total: 0, gated: true, contributions: [] };
      partial[s.id] = s.compute(doc, full);
    }
    const total = combine(this.combiner, partial, this.weights);
    return {
      docId: productId,
      total,
      contributions: Object.entries(partial).map(([signalId, value]) => ({
        signalId,
        value,
        weight: this.weights[signalId] ?? 1
      }))
    };
  }
  rank(ctx, filter, topK) {
    const results = [];
    for (const doc of this.catalog.allDocs()) {
      if (!filter(doc)) continue;
      let passed = true;
      const partial = {};
      for (const s of this.signals) {
        if (s.gate && !s.gate(doc, ctx)) {
          passed = false;
          break;
        }
        partial[s.id] = s.compute(doc, ctx);
      }
      if (!passed) continue;
      const score = combine(this.combiner, partial, this.weights);
      results.push({ doc, score });
    }
    results.sort((a, b) => b.score - a.score);
    const div = this.opts.diversity;
    if (div) {
      const pool = results.slice(0, div.poolSize ?? topK * 5);
      const picked = [];
      const lambda = 1 - (div.weight ?? 0.3);
      while (picked.length < topK && pool.length > 0) {
        let bestIdx = 0;
        let bestMmr = -Infinity;
        for (let i = 0; i < pool.length; i++) {
          const cand = pool[i];
          const candValue = String(cand.doc.meta?.[div.field] ?? "");
          let maxSim = 0;
          for (const p of picked) {
            const pv = String(p.doc.meta?.[div.field] ?? "");
            if (pv && candValue && pv === candValue) {
              maxSim = 1;
              break;
            }
          }
          const mmr = lambda * cand.score - (1 - lambda) * maxSim * (results[0]?.score ?? 1);
          if (mmr > bestMmr) {
            bestMmr = mmr;
            bestIdx = i;
          }
        }
        picked.push(pool.splice(bestIdx, 1)[0]);
      }
      return picked;
    }
    return results.slice(0, topK);
  }
  async dispose() {
    if (this.opened) await this.storage.close();
    this.opened = false;
  }
};
function contentSimilarity(opts) {
  return {
    id: "content_similarity",
    compute(doc, ctx) {
      const sourceId = Array.isArray(ctx.sourceProductId) ? ctx.sourceProductId[0] : ctx.sourceProductId;
      if (!sourceId) return 0;
      const sFeats = opts.source.getFeatures(sourceId);
      const tFeats = opts.source.getFeatures(doc.id);
      if (!sFeats || !tFeats) return 0;
      const sSet = new Set(sFeats);
      let overlap = 0;
      for (const f of tFeats) if (sSet.has(f)) overlap++;
      const denom = Math.sqrt(sFeats.length * tFeats.length) || 1;
      return overlap / denom;
    }
  };
}
function recencyDecay2(opts = {}) {
  const field = opts.field ?? "listed_at";
  const half = (opts.halfLifeDays ?? 90) * 864e5;
  return {
    id: "recency_decay",
    compute(doc, ctx) {
      const ts = Number(doc.meta?.[field] ?? 0);
      if (!ts) return 1;
      const age = Math.max(0, ctx.now - ts);
      return Math.pow(0.5, age / half);
    }
  };
}
function coldStartBayesian(opts = {}) {
  const k = opts.priorWeight ?? 50;
  const prior = opts.priorRate ?? 0.5;
  const yes = opts.yesField ?? "lifetime_yes_count";
  const events = opts.eventsField ?? "lifetime_event_count";
  return {
    id: "cold_start_bayesian",
    compute(doc) {
      const y = Number(doc.meta?.[yes] ?? 0);
      const e = Number(doc.meta?.[events] ?? 0);
      return (y + prior * k) / (e + k);
    }
  };
}
function saturation(opts) {
  const cap = opts.cap ?? 200;
  return {
    id: "saturation_" + opts.field,
    compute(doc) {
      const v = Number(doc.meta?.[opts.field] ?? 0);
      return cap * (1 - Math.exp(-v / cap));
    }
  };
}
function promoBoost2(opts = {}) {
  const field = opts.field ?? "promo_boost";
  return {
    id: "promo_boost",
    compute(doc) {
      const v = Number(doc.meta?.[field] ?? 1);
      return v > 0 ? v : 1;
    }
  };
}
function preferenceMatch(opts = {}) {
  const yesBoost = opts.yesBoost ?? 2;
  const noBoost = opts.noBoost ?? 0.3;
  return {
    id: "preference_match",
    compute(doc, ctx) {
      const prefs = ctx.prefs ?? [];
      if (prefs.length === 0) return 1;
      const targetFeats = new Set(opts.catalog.getFeatures(doc.id) ?? []);
      let yesOverlap = 0;
      let noOverlap = 0;
      for (const p of prefs) {
        const f = opts.catalog.getFeatures(p.productId);
        if (!f) continue;
        let overlap = 0;
        for (const x of f) if (targetFeats.has(x)) overlap++;
        if (p.response === "yes") yesOverlap += overlap;
        else if (p.response === "no") noOverlap += overlap;
      }
      const yesFactor = 1 + (yesBoost - 1) * Math.tanh(yesOverlap / 10);
      const noFactor = noBoost + (1 - noBoost) * Math.exp(-noOverlap / 10);
      return yesFactor * noFactor;
    }
  };
}
function diversityPenalty(opts) {
  void opts;
  return {
    id: "diversity_penalty",
    compute: () => 1
  };
}
var builtin3 = {
  contentSimilarity,
  recencyDecay: recencyDecay2,
  coldStartBayesian,
  saturation,
  promoBoost: promoBoost2,
  preferenceMatch,
  diversityPenalty
};
function createRecommend(opts) {
  return new Recommend(opts);
}

// src/toolbox/common/index.ts
var common_exports = {};
__export(common_exports, {
  clamp: () => clamp,
  combine: () => combine,
  identityAdapter: () => identityAdapter,
  nfkc: () => nfkc
});
export {
  common_exports as common,
  recommend_exports as recommend,
  search_exports as search
};
