@yurtsever/capsa 0.1.0-alpha.3 → 0.1.0-alpha.4

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 (2) hide show
  1. package/dist/index.js +93 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10227,6 +10227,10 @@ var Store = class {
10227
10227
  }
10228
10228
  }));
10229
10229
  }
10230
+ /** All chunks of one item, in document order. */
10231
+ chunksForItem(itemId) {
10232
+ return this.db.prepare("SELECT id, item_id, idx, heading_path, text FROM chunks WHERE item_id = ? ORDER BY idx").all(itemId);
10233
+ }
10230
10234
  listItems() {
10231
10235
  return this.db.prepare("SELECT * FROM items ORDER BY kind, rel_path").all();
10232
10236
  }
@@ -10566,6 +10570,40 @@ function rrf(lists, k = DEFAULT_WEIGHTS.rrfK) {
10566
10570
  return scores;
10567
10571
  }
10568
10572
 
10573
+ // ../index/dist/pin.js
10574
+ var LEADING_ID = /^([A-Za-z]{1,8}-?\d{1,5}(?:-\d{1,5})?|\d{2,6})(?=[-_.\s]|$)/;
10575
+ function leadingId(relPath) {
10576
+ const base = relPath.split(/[\\/]/).pop() ?? "";
10577
+ return LEADING_ID.exec(base)?.[1];
10578
+ }
10579
+ function pinnedItems(query, items) {
10580
+ const q = query.toLowerCase();
10581
+ const idCount = /* @__PURE__ */ new Map();
10582
+ for (const item of items) {
10583
+ const id = leadingId(item.rel_path)?.toLowerCase();
10584
+ if (id)
10585
+ idCount.set(id, (idCount.get(id) ?? 0) + 1);
10586
+ }
10587
+ const out = [];
10588
+ for (const item of items) {
10589
+ const rel = item.rel_path.toLowerCase();
10590
+ if (q.includes(rel)) {
10591
+ out.push(item);
10592
+ continue;
10593
+ }
10594
+ const id = leadingId(item.rel_path);
10595
+ if (!id || (idCount.get(id.toLowerCase()) ?? 0) > 1)
10596
+ continue;
10597
+ const re = new RegExp(`(^|[^\\w-])${escape2(id)}(?=$|[^\\w-])`, "i");
10598
+ if (re.test(query))
10599
+ out.push(item);
10600
+ }
10601
+ return out;
10602
+ }
10603
+ function escape2(s) {
10604
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10605
+ }
10606
+
10569
10607
  // ../index/dist/retrieve.js
10570
10608
  async function retrieve(store, embedder, query, options = {}) {
10571
10609
  const started = Date.now();
@@ -10573,31 +10611,73 @@ async function retrieve(store, embedder, query, options = {}) {
10573
10611
  const candidates = options.candidates ?? 30;
10574
10612
  const weights = options.weights ?? DEFAULT_WEIGHTS;
10575
10613
  const minRelative = options.minRelativeScore ?? 0.5;
10614
+ const perItem = /* @__PURE__ */ new Map();
10615
+ const chunks = [];
10616
+ const debug = [];
10617
+ let tokens = 0;
10618
+ const pinned = pinnedItems(query, store.listItems());
10619
+ for (const item of pinned) {
10620
+ for (const c of store.chunksForItem(item.id)) {
10621
+ const t = estimateTokens(c.text);
10622
+ const fits = tokens + t <= maxTokens || chunks.length === 0;
10623
+ if (options.debug) {
10624
+ debug.push({
10625
+ chunkId: c.id,
10626
+ relPath: item.rel_path,
10627
+ headingPath: JSON.parse(c.heading_path),
10628
+ kind: item.kind,
10629
+ status: item.status,
10630
+ vecRank: null,
10631
+ ftsRank: null,
10632
+ fused: 0,
10633
+ factor: 1,
10634
+ score: Number.POSITIVE_INFINITY,
10635
+ kept: fits,
10636
+ why: fits ? "pinned" : "budget"
10637
+ });
10638
+ }
10639
+ if (!fits)
10640
+ continue;
10641
+ perItem.set(item.id, (perItem.get(item.id) ?? 0) + 1);
10642
+ tokens += t;
10643
+ chunks.push({
10644
+ chunkId: c.id,
10645
+ score: Number.POSITIVE_INFINITY,
10646
+ kind: item.kind,
10647
+ status: item.status,
10648
+ updatedAt: item.updated_at,
10649
+ relPath: item.rel_path,
10650
+ headingPath: JSON.parse(c.heading_path),
10651
+ text: c.text,
10652
+ tokens: t
10653
+ });
10654
+ }
10655
+ }
10656
+ const pinnedIds = new Set(pinned.map((i) => i.id));
10657
+ const neighbourCutoff = pinned.length ? Math.max(minRelative, 0.75) : minRelative;
10658
+ const neighbourBudget = pinned.length ? Math.min(maxTokens, tokens + Math.floor(maxTokens / 3)) : maxTokens;
10576
10659
  const [qv] = await embedder.embed([query]);
10577
10660
  const vec = qv ? store.vectorSearch(qv, candidates).map((r) => r.chunkId) : [];
10578
10661
  const fts = store.textSearch(query, candidates).map((r) => r.chunkId);
10579
10662
  const fused = rrf([vec, fts], weights.rrfK);
10580
- const rows = store.chunksWithItems([...fused.keys()]);
10663
+ const rows = store.chunksWithItems([...fused.keys()]).filter((r) => !pinnedIds.has(r.item_id));
10581
10664
  const now = Date.now();
10582
10665
  const scored = rows.map((r) => {
10583
10666
  const fusedScore = fused.get(r.id) ?? 0;
10584
10667
  const factor = signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now);
10585
10668
  return { row: r, fusedScore, factor, score: fusedScore * factor };
10586
10669
  }).sort((a, b) => b.score - a.score);
10587
- const perItem = /* @__PURE__ */ new Map();
10588
- const chunks = [];
10589
- const debug = [];
10590
- let tokens = 0;
10591
10670
  const best = scored[0]?.score ?? 0;
10592
- let cut = false;
10671
+ let cut = tokens >= maxTokens;
10672
+ let kept = 0;
10593
10673
  for (const { row, fusedScore, factor, score } of scored) {
10594
10674
  let why = "kept";
10595
- if (cut || chunks.length > 0 && score < best * minRelative) {
10675
+ if (cut || kept > 0 && score < best * neighbourCutoff) {
10596
10676
  cut = true;
10597
10677
  why = "cutoff";
10598
10678
  } else if ((perItem.get(row.item_id) ?? 0) >= 2) {
10599
10679
  why = "per-item cap";
10600
- } else if (tokens + estimateTokens(row.text) > maxTokens && chunks.length > 0) {
10680
+ } else if (tokens + estimateTokens(row.text) > neighbourBudget && chunks.length > 0) {
10601
10681
  why = "budget";
10602
10682
  }
10603
10683
  if (options.debug) {
@@ -10621,6 +10701,7 @@ async function retrieve(store, embedder, query, options = {}) {
10621
10701
  const t = estimateTokens(row.text);
10622
10702
  perItem.set(row.item_id, (perItem.get(row.item_id) ?? 0) + 1);
10623
10703
  tokens += t;
10704
+ kept++;
10624
10705
  chunks.push({
10625
10706
  chunkId: row.id,
10626
10707
  score,
@@ -10632,7 +10713,7 @@ async function retrieve(store, embedder, query, options = {}) {
10632
10713
  text: row.text,
10633
10714
  tokens: t
10634
10715
  });
10635
- if (tokens >= maxTokens)
10716
+ if (tokens >= neighbourBudget)
10636
10717
  cut = true;
10637
10718
  }
10638
10719
  const durationMs = Date.now() - started;
@@ -10652,15 +10733,15 @@ function rankIn(list, id) {
10652
10733
  function formatDebug(result) {
10653
10734
  if (!result.debug)
10654
10735
  return "";
10655
- const lines = [" vec fts factor score keep where"];
10736
+ const lines = [" vec fts factor score keep where"];
10656
10737
  for (const d of result.debug) {
10657
10738
  const where = [d.relPath, ...d.headingPath].join(" \u203A ").slice(0, 70);
10658
10739
  lines.push([
10659
10740
  String(d.vecRank ?? "-").padStart(5),
10660
10741
  String(d.ftsRank ?? "-").padStart(4),
10661
10742
  d.factor.toFixed(2).padStart(7),
10662
- d.score.toFixed(4).padStart(8),
10663
- (d.kept ? "yes" : d.why).padEnd(12),
10743
+ (Number.isFinite(d.score) ? d.score.toFixed(4) : "pin").padStart(8),
10744
+ (d.kept ? d.why === "pinned" ? "pinned" : "yes" : d.why).padEnd(12),
10664
10745
  where
10665
10746
  ].join(" "));
10666
10747
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yurtsever/capsa",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Your project's knowledge in one capsule. Local-first project memory and cockpit for AI coding agents: indexes runbooks, tickets, plans and instruction files, serves the smallest relevant context over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",