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

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 +120 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10053,7 +10053,7 @@ function loadSqlite() {
10053
10053
  }
10054
10054
  var DB_DIR = ".capsa";
10055
10055
  var DB_FILE = "index.db";
10056
- var SCHEMA_VERSION = 2;
10056
+ var SCHEMA_VERSION = 3;
10057
10057
  function dbPath(projectRoot) {
10058
10058
  return join6(projectRoot, DB_DIR, DB_FILE);
10059
10059
  }
@@ -10075,9 +10075,20 @@ var Store = class {
10075
10075
  const row = this.db.prepare("SELECT value FROM meta WHERE key = 'schema'").get();
10076
10076
  return row?.value !== String(SCHEMA_VERSION);
10077
10077
  }
10078
- /** Drop all indexed content (not the log, not decisions) and mark the schema current. */
10078
+ /**
10079
+ * Drop all indexed content (not the log, not decisions) and mark the schema
10080
+ * current. The virtual tables are dropped and recreated, not emptied:
10081
+ * `CREATE VIRTUAL TABLE IF NOT EXISTS` would keep an old tokenizer alive
10082
+ * through a rebuild — which once left FTS without stemming after v2.
10083
+ */
10079
10084
  reset() {
10080
- this.db.exec("DELETE FROM chunks_vec; DELETE FROM chunks_fts; DELETE FROM chunks; DELETE FROM items;");
10085
+ this.db.exec(`
10086
+ DROP TABLE IF EXISTS chunks_fts;
10087
+ DROP TABLE IF EXISTS chunks_vec;
10088
+ DELETE FROM chunks;
10089
+ DELETE FROM items;
10090
+ `);
10091
+ this.migrate();
10081
10092
  this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES ('schema', ?)").run(String(SCHEMA_VERSION));
10082
10093
  }
10083
10094
  migrate() {
@@ -10568,24 +10579,47 @@ async function retrieve(store, embedder, query, options = {}) {
10568
10579
  const fused = rrf([vec, fts], weights.rrfK);
10569
10580
  const rows = store.chunksWithItems([...fused.keys()]);
10570
10581
  const now = Date.now();
10571
- const scored = rows.map((r) => ({
10572
- row: r,
10573
- score: (fused.get(r.id) ?? 0) * signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now)
10574
- })).sort((a, b) => b.score - a.score);
10582
+ const scored = rows.map((r) => {
10583
+ const fusedScore = fused.get(r.id) ?? 0;
10584
+ const factor = signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now);
10585
+ return { row: r, fusedScore, factor, score: fusedScore * factor };
10586
+ }).sort((a, b) => b.score - a.score);
10575
10587
  const perItem = /* @__PURE__ */ new Map();
10576
10588
  const chunks = [];
10589
+ const debug = [];
10577
10590
  let tokens = 0;
10578
10591
  const best = scored[0]?.score ?? 0;
10579
- for (const { row, score } of scored) {
10580
- if (chunks.length > 0 && score < best * minRelative)
10581
- break;
10582
- const used = perItem.get(row.item_id) ?? 0;
10583
- if (used >= 2)
10592
+ let cut = false;
10593
+ for (const { row, fusedScore, factor, score } of scored) {
10594
+ let why = "kept";
10595
+ if (cut || chunks.length > 0 && score < best * minRelative) {
10596
+ cut = true;
10597
+ why = "cutoff";
10598
+ } else if ((perItem.get(row.item_id) ?? 0) >= 2) {
10599
+ why = "per-item cap";
10600
+ } else if (tokens + estimateTokens(row.text) > maxTokens && chunks.length > 0) {
10601
+ why = "budget";
10602
+ }
10603
+ if (options.debug) {
10604
+ debug.push({
10605
+ chunkId: row.id,
10606
+ relPath: row.item.rel_path,
10607
+ headingPath: JSON.parse(row.heading_path),
10608
+ kind: row.item.kind,
10609
+ status: row.item.status,
10610
+ vecRank: rankIn(vec, row.id),
10611
+ ftsRank: rankIn(fts, row.id),
10612
+ fused: fusedScore,
10613
+ factor,
10614
+ score,
10615
+ kept: why === "kept",
10616
+ why
10617
+ });
10618
+ }
10619
+ if (why !== "kept")
10584
10620
  continue;
10585
10621
  const t = estimateTokens(row.text);
10586
- if (tokens + t > maxTokens && chunks.length > 0)
10587
- continue;
10588
- perItem.set(row.item_id, used + 1);
10622
+ perItem.set(row.item_id, (perItem.get(row.item_id) ?? 0) + 1);
10589
10623
  tokens += t;
10590
10624
  chunks.push({
10591
10625
  chunkId: row.id,
@@ -10599,7 +10633,7 @@ async function retrieve(store, embedder, query, options = {}) {
10599
10633
  tokens: t
10600
10634
  });
10601
10635
  if (tokens >= maxTokens)
10602
- break;
10636
+ cut = true;
10603
10637
  }
10604
10638
  const durationMs = Date.now() - started;
10605
10639
  store.logContext({
@@ -10609,7 +10643,28 @@ async function retrieve(store, embedder, query, options = {}) {
10609
10643
  tokensEst: tokens,
10610
10644
  durationMs
10611
10645
  });
10612
- return { query, chunks, tokensEst: tokens, durationMs };
10646
+ return { query, chunks, tokensEst: tokens, durationMs, ...options.debug ? { debug } : {} };
10647
+ }
10648
+ function rankIn(list, id) {
10649
+ const i = list.indexOf(id);
10650
+ return i === -1 ? null : i + 1;
10651
+ }
10652
+ function formatDebug(result) {
10653
+ if (!result.debug)
10654
+ return "";
10655
+ const lines = [" vec fts factor score keep where"];
10656
+ for (const d of result.debug) {
10657
+ const where = [d.relPath, ...d.headingPath].join(" \u203A ").slice(0, 70);
10658
+ lines.push([
10659
+ String(d.vecRank ?? "-").padStart(5),
10660
+ String(d.ftsRank ?? "-").padStart(4),
10661
+ d.factor.toFixed(2).padStart(7),
10662
+ d.score.toFixed(4).padStart(8),
10663
+ (d.kept ? "yes" : d.why).padEnd(12),
10664
+ where
10665
+ ].join(" "));
10666
+ }
10667
+ return lines.join("\n");
10613
10668
  }
10614
10669
  function formatContext(result) {
10615
10670
  if (result.chunks.length === 0)
@@ -10631,7 +10686,8 @@ function projectState(store) {
10631
10686
  for (const i of items)
10632
10687
  counts[i.kind] = (counts[i.kind] ?? 0) + 1;
10633
10688
  const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
10634
- const open2 = items.filter(live).sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")).map((i) => ({ relPath: i.rel_path, name: i.name, status: i.status ?? "unknown", updatedAt: i.updated_at }));
10689
+ const rank = (i) => i.status === "in-progress" ? 0 : 1;
10690
+ const open2 = items.filter(live).sort((a, b) => rank(a) - rank(b) || a.rel_path.localeCompare(b.rel_path)).map((i) => ({ relPath: i.rel_path, name: i.name, status: i.status ?? "unknown", updatedAt: i.updated_at }));
10635
10691
  const recentlyChanged = [...items].sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")).slice(0, 10).map((i) => ({ relPath: i.rel_path, kind: i.kind, updatedAt: i.updated_at }));
10636
10692
  const decisions = store.db.prepare("SELECT id, created_at, title FROM decisions ORDER BY id DESC LIMIT 10").all().map((d) => ({ id: d.id, createdAt: d.created_at, title: d.title }));
10637
10693
  return { counts, open: open2, recentlyChanged, decisions };
@@ -40000,17 +40056,37 @@ async function guarded(run) {
40000
40056
  process.exit(1);
40001
40057
  }
40002
40058
  }
40003
- function rootFrom(args) {
40004
- const positional = args.filter((a) => !a.startsWith("--"));
40005
- return resolve3(positional[0] ?? process.cwd());
40059
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--model", "--tokens"]);
40060
+ function parseArgs(args) {
40061
+ const positional = [];
40062
+ const flags = /* @__PURE__ */ new Map();
40063
+ for (let i = 0; i < args.length; i++) {
40064
+ const arg = args[i];
40065
+ if (VALUE_FLAGS.has(arg)) {
40066
+ const value = args[++i];
40067
+ if (value === void 0 || value.startsWith("--")) {
40068
+ throw new Error(`${arg} expects a value`);
40069
+ }
40070
+ flags.set(arg, value);
40071
+ } else if (arg.startsWith("--")) {
40072
+ flags.set(arg, "true");
40073
+ } else {
40074
+ positional.push(arg);
40075
+ }
40076
+ }
40077
+ return { positional, flags };
40006
40078
  }
40007
- function flag(args, name) {
40008
- const i = args.indexOf(name);
40009
- return i >= 0 ? args[i + 1] : void 0;
40079
+ function tokensFlag(flags) {
40080
+ const raw2 = flags.get("--tokens");
40081
+ if (raw2 === void 0) return void 0;
40082
+ const n = Number(raw2);
40083
+ if (!Number.isInteger(n) || n < 200) throw new Error(`--tokens expects an integer \u2265 200, got "${raw2}"`);
40084
+ return n;
40010
40085
  }
40011
40086
  async function runIndexCommand(args) {
40012
- const root = rootFrom(args);
40013
- const embedder = ollamaEmbedder({ model: flag(args, "--model") });
40087
+ const { positional, flags } = parseArgs(args);
40088
+ const root = resolve3(positional[0] ?? process.cwd());
40089
+ const embedder = ollamaEmbedder({ model: flags.get("--model") });
40014
40090
  console.error(`capsa: indexing ${root} with ${embedder.model} \u2026`);
40015
40091
  const res = await indexProject(root, embedder, (p) => {
40016
40092
  if (p.phase === "rebuild") console.error("capsa: index schema changed, rebuilding from scratch \u2026");
@@ -40023,19 +40099,25 @@ async function runIndexCommand(args) {
40023
40099
  for (const i of res.issues.slice(0, 10)) console.error(` ! ${i.path}: ${i.message}`);
40024
40100
  }
40025
40101
  async function runSearchCommand(args) {
40026
- const positional = args.filter((a) => !a.startsWith("--"));
40102
+ const { positional, flags } = parseArgs(args);
40027
40103
  const query = positional[0];
40028
40104
  if (!query) {
40029
- console.error("capsa search <query> [path]");
40105
+ console.error("capsa search <query> [path] [--tokens n] [--model name] [--debug]");
40030
40106
  process.exit(1);
40031
40107
  }
40032
40108
  const root = resolve3(positional[1] ?? process.cwd());
40033
- const embedder = ollamaEmbedder({ model: flag(args, "--model") });
40034
- const maxTokens = flag(args, "--tokens") ? Number(flag(args, "--tokens")) : void 0;
40109
+ const embedder = ollamaEmbedder({ model: flags.get("--model") });
40110
+ const maxTokens = tokensFlag(flags);
40035
40111
  const store = openStore(root, embedder);
40036
40112
  try {
40037
- const res = await retrieve(store, embedder, query, { maxTokens, source: "cli:search" });
40038
- console.log(formatContext(res));
40113
+ const debug = flags.get("--debug") === "true";
40114
+ const res = await retrieve(store, embedder, query, { maxTokens, source: "cli:search", debug });
40115
+ if (debug) {
40116
+ console.log(formatDebug(res));
40117
+ console.log("");
40118
+ } else {
40119
+ console.log(formatContext(res));
40120
+ }
40039
40121
  console.error(`
40040
40122
  ${res.chunks.length} chunks, ~${res.tokensEst} tokens, ${res.durationMs} ms`);
40041
40123
  } finally {
@@ -40043,7 +40125,7 @@ ${res.chunks.length} chunks, ~${res.tokensEst} tokens, ${res.durationMs} ms`);
40043
40125
  }
40044
40126
  }
40045
40127
  async function runStateCommand(args) {
40046
- const root = rootFrom(args);
40128
+ const root = resolve3(parseArgs(args).positional[0] ?? process.cwd());
40047
40129
  const store = openStore(root, ollamaEmbedder());
40048
40130
  try {
40049
40131
  console.log(formatState(projectState(store)));
@@ -40052,7 +40134,11 @@ async function runStateCommand(args) {
40052
40134
  }
40053
40135
  }
40054
40136
  async function runMcpCommand(args) {
40055
- await serveStdio({ projectRoot: rootFrom(args) });
40137
+ const { positional, flags } = parseArgs(args);
40138
+ await serveStdio({
40139
+ projectRoot: resolve3(positional[0] ?? process.cwd()),
40140
+ embedder: ollamaEmbedder({ model: flags.get("--model") })
40141
+ });
40056
40142
  }
40057
40143
 
40058
40144
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yurtsever/capsa",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
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",