@yurtsever/capsa 0.1.0-alpha.0 → 0.1.0-alpha.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 (2) hide show
  1. package/dist/index.js +39 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10053,6 +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
10057
  function dbPath(projectRoot) {
10057
10058
  return join6(projectRoot, DB_DIR, DB_FILE);
10058
10059
  }
@@ -10069,6 +10070,16 @@ var Store = class {
10069
10070
  this.db.exec("PRAGMA journal_mode = WAL");
10070
10071
  this.migrate();
10071
10072
  }
10073
+ /** True when this index was written by an older schema and must be rebuilt. */
10074
+ needsRebuild() {
10075
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = 'schema'").get();
10076
+ return row?.value !== String(SCHEMA_VERSION);
10077
+ }
10078
+ /** Drop all indexed content (not the log, not decisions) and mark the schema current. */
10079
+ reset() {
10080
+ this.db.exec("DELETE FROM chunks_vec; DELETE FROM chunks_fts; DELETE FROM chunks; DELETE FROM items;");
10081
+ this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES ('schema', ?)").run(String(SCHEMA_VERSION));
10082
+ }
10072
10083
  migrate() {
10073
10084
  this.db.exec(`
10074
10085
  CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
@@ -10097,7 +10108,10 @@ var Store = class {
10097
10108
  );
10098
10109
  CREATE INDEX IF NOT EXISTS chunks_item ON chunks(item_id);
10099
10110
 
10100
- CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(text, tokenize = 'unicode61');
10111
+ -- porter: "enforcement" must find "Enforce". The indexed text is the
10112
+ -- chunk plus its provenance header (title, heading trail) so a section
10113
+ -- called "Acceptance criteria" still knows which ticket it belongs to.
10114
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(text, tokenize = 'porter unicode61');
10101
10115
 
10102
10116
  CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
10103
10117
  chunk_id INTEGER PRIMARY KEY,
@@ -10151,10 +10165,14 @@ var Store = class {
10151
10165
  status = excluded.status, updated_at = excluded.updated_at, tags = excluded.tags,
10152
10166
  indexed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')`).run(row.id, row.format, row.kind, row.name, row.path, row.rel_path, row.content_hash, row.status, row.updated_at, row.tags);
10153
10167
  }
10154
- insertChunk(itemId, idx, headingPath, text, embedding) {
10168
+ /**
10169
+ * `text` is what the user sees; `searchText` (chunk + provenance header)
10170
+ * is what FTS and the embedder see. Falls back to `text` when omitted.
10171
+ */
10172
+ insertChunk(itemId, idx, headingPath, text, embedding, searchText = text) {
10155
10173
  const res = this.db.prepare("INSERT INTO chunks (item_id, idx, heading_path, text) VALUES (?, ?, ?, ?)").run(itemId, idx, JSON.stringify(headingPath), text);
10156
10174
  const id = Number(res.lastInsertRowid);
10157
- this.db.prepare("INSERT INTO chunks_fts (rowid, text) VALUES (?, ?)").run(id, text);
10175
+ this.db.prepare("INSERT INTO chunks_fts (rowid, text) VALUES (?, ?)").run(id, searchText);
10158
10176
  this.db.prepare("INSERT INTO chunks_vec (chunk_id, embedding) VALUES (?, ?)").run(BigInt(id), new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength));
10159
10177
  return id;
10160
10178
  }
@@ -10221,7 +10239,7 @@ import { relative as relative5 } from "path";
10221
10239
  import { readFile as readFile5, stat as stat3 } from "fs/promises";
10222
10240
  import { basename as basename4, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
10223
10241
  var DOC_DIRS = /* @__PURE__ */ new Set(["docs", "doc", "runbooks", "runbook", "wiki", "adr", "decisions"]);
10224
- var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "plans", "node_modules"]);
10242
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog", "plans", "plan", "roadmap", "milestones", "milestone", "node_modules"]);
10225
10243
  function segments(relPath) {
10226
10244
  return relPath.split(/[\\/]/).filter(Boolean);
10227
10245
  }
@@ -10294,7 +10312,7 @@ function firstHeading(raw2) {
10294
10312
  import { readFile as readFile6, stat as stat4 } from "fs/promises";
10295
10313
  import { basename as basename5, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
10296
10314
  var TICKET_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog"]);
10297
- var PLAN_DIRS = /* @__PURE__ */ new Set(["plans", "plan", "roadmap"]);
10315
+ var PLAN_DIRS = /* @__PURE__ */ new Set(["plans", "plan", "roadmap", "milestones", "milestone"]);
10298
10316
  var CHECKBOX = /^\s*[-*+]\s+\[( |x|X)\]\s+/gm;
10299
10317
  function segments2(relPath) {
10300
10318
  return relPath.split(/[\\/]/).filter(Boolean);
@@ -10414,6 +10432,10 @@ async function indexProject(projectRoot, embedder, onProgress) {
10414
10432
  const started = Date.now();
10415
10433
  const store = openStore(projectRoot, embedder);
10416
10434
  try {
10435
+ if (store.needsRebuild()) {
10436
+ onProgress?.({ phase: "rebuild" });
10437
+ store.reset();
10438
+ }
10417
10439
  onProgress?.({ phase: "scan" });
10418
10440
  const result = await scan(knowledgeAdapters(), { roots: [projectRoot] });
10419
10441
  const items = result.items.filter((i) => i.scope === "project" && isInside(projectRoot, i.path));
@@ -10431,8 +10453,8 @@ async function indexProject(projectRoot, embedder, onProgress) {
10431
10453
  if (existing)
10432
10454
  store.deleteItem(existing.id);
10433
10455
  const chunks = chunkMarkdown(item.content);
10434
- const texts = chunks.map((c) => withContext(item, c.headingPath, c.text));
10435
- const vectors = texts.length ? await embedder.embed(texts) : [];
10456
+ const searchTexts = chunks.map((c) => withContext(item, c.headingPath, c.text));
10457
+ const vectors = searchTexts.length ? await embedder.embed(searchTexts) : [];
10436
10458
  const k = item.knowledge;
10437
10459
  store.upsertItem({
10438
10460
  id: item.id,
@@ -10446,7 +10468,7 @@ async function indexProject(projectRoot, embedder, onProgress) {
10446
10468
  updated_at: k?.updatedAt ?? null,
10447
10469
  tags: k?.tags ? JSON.stringify(k.tags) : null
10448
10470
  });
10449
- chunks.forEach((c, i) => store.insertChunk(item.id, c.index, c.headingPath, c.text, vectors[i]));
10471
+ chunks.forEach((c, i) => store.insertChunk(item.id, c.index, c.headingPath, c.text, vectors[i], searchTexts[i]));
10450
10472
  indexed++;
10451
10473
  onProgress?.({ phase: "embed", scanned: items.length, indexed, skipped });
10452
10474
  }
@@ -10503,6 +10525,7 @@ var DEFAULT_WEIGHTS = {
10503
10525
  plan: 1.05,
10504
10526
  ticket: 1,
10505
10527
  runbook: 1,
10528
+ doc: 0.95,
10506
10529
  wiki: 0.95,
10507
10530
  commit: 0.85
10508
10531
  }
@@ -10518,7 +10541,9 @@ function recencyFactor(updatedAt, w, now = Date.now()) {
10518
10541
  return w.recencyFloor + (1 - w.recencyFloor) * decay;
10519
10542
  }
10520
10543
  function signalFactor(s, w = DEFAULT_WEIGHTS, now = Date.now()) {
10521
- return recencyFactor(s.updatedAt, w, now) * w.status[s.status ?? "unknown"] * w.kind[s.kind];
10544
+ const status = w.status[s.status ?? "unknown"] ?? 1;
10545
+ const kind = w.kind[s.kind] ?? 1;
10546
+ return recencyFactor(s.updatedAt, w, now) * status * kind;
10522
10547
  }
10523
10548
  function rrf(lists, k = DEFAULT_WEIGHTS.rrfK) {
10524
10549
  const scores = /* @__PURE__ */ new Map();
@@ -10536,6 +10561,7 @@ async function retrieve(store, embedder, query, options = {}) {
10536
10561
  const maxTokens = options.maxTokens ?? 1500;
10537
10562
  const candidates = options.candidates ?? 30;
10538
10563
  const weights = options.weights ?? DEFAULT_WEIGHTS;
10564
+ const minRelative = options.minRelativeScore ?? 0.5;
10539
10565
  const [qv] = await embedder.embed([query]);
10540
10566
  const vec = qv ? store.vectorSearch(qv, candidates).map((r) => r.chunkId) : [];
10541
10567
  const fts = store.textSearch(query, candidates).map((r) => r.chunkId);
@@ -10549,7 +10575,10 @@ async function retrieve(store, embedder, query, options = {}) {
10549
10575
  const perItem = /* @__PURE__ */ new Map();
10550
10576
  const chunks = [];
10551
10577
  let tokens = 0;
10578
+ const best = scored[0]?.score ?? 0;
10552
10579
  for (const { row, score } of scored) {
10580
+ if (chunks.length > 0 && score < best * minRelative)
10581
+ break;
10553
10582
  const used = perItem.get(row.item_id) ?? 0;
10554
10583
  if (used >= 2)
10555
10584
  continue;
@@ -39984,6 +40013,7 @@ async function runIndexCommand(args) {
39984
40013
  const embedder = ollamaEmbedder({ model: flag(args, "--model") });
39985
40014
  console.error(`capsa: indexing ${root} with ${embedder.model} \u2026`);
39986
40015
  const res = await indexProject(root, embedder, (p) => {
40016
+ if (p.phase === "rebuild") console.error("capsa: index schema changed, rebuilding from scratch \u2026");
39987
40017
  if (p.phase === "embed") process.stderr.write(`\r embedded ${p.indexed} / ${p.scanned} (skipped ${p.skipped}) `);
39988
40018
  });
39989
40019
  process.stderr.write("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yurtsever/capsa",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
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",