@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.
- package/dist/index.js +39 -9
- 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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
10435
|
-
const vectors =
|
|
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
|
-
|
|
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.
|
|
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",
|