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

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 +370 -53
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7228,6 +7228,9 @@ var DEFAULT_EXCLUDES = [
7228
7228
  ".Trash"
7229
7229
  ];
7230
7230
  var DEFAULT_MAX_DEPTH = 12;
7231
+ function isSourceAdapter(adapter) {
7232
+ return typeof adapter.collect === "function";
7233
+ }
7231
7234
  function stableId(absPath) {
7232
7235
  return createHash("sha1").update(absPath).digest("hex").slice(0, 12);
7233
7236
  }
@@ -7246,16 +7249,24 @@ async function scan(adapters, options = {}) {
7246
7249
  const items = [];
7247
7250
  const seenItemPaths = /* @__PURE__ */ new Set();
7248
7251
  const scannedRoots = [];
7252
+ const formatAdapters = adapters.filter((a) => !isSourceAdapter(a));
7253
+ const sourceAdapters = adapters.filter(isSourceAdapter);
7249
7254
  const rootSet = /* @__PURE__ */ new Set();
7250
7255
  for (const r of options.roots ?? [env.homeDir])
7251
7256
  rootSet.add(resolve(r));
7252
- for (const adapter of adapters) {
7257
+ for (const adapter of formatAdapters) {
7253
7258
  for (const r of adapter.globalRoots?.(env) ?? [])
7254
7259
  rootSet.add(resolve(r));
7255
7260
  }
7256
7261
  const roots = [...rootSet].filter((r) => ![...rootSet].some((other) => other !== r && r.startsWith(other + "/")));
7262
+ const addItem = (item) => {
7263
+ if (seenItemPaths.has(item.path))
7264
+ return;
7265
+ seenItemPaths.add(item.path);
7266
+ items.push(item);
7267
+ };
7257
7268
  const handleEntry = async (entry) => {
7258
- for (const adapter of adapters) {
7269
+ for (const adapter of formatAdapters) {
7259
7270
  let matched = false;
7260
7271
  try {
7261
7272
  matched = adapter.matches(entry);
@@ -7270,12 +7281,8 @@ async function scan(adapters, options = {}) {
7270
7281
  if (!matched)
7271
7282
  continue;
7272
7283
  try {
7273
- for (const item of await adapter.parse(entry, env)) {
7274
- if (seenItemPaths.has(item.path))
7275
- continue;
7276
- seenItemPaths.add(item.path);
7277
- items.push(item);
7278
- }
7284
+ for (const item of await adapter.parse(entry, env))
7285
+ addItem(item);
7279
7286
  } catch (err) {
7280
7287
  issues.push({
7281
7288
  path: entry.absPath,
@@ -7346,6 +7353,18 @@ async function scan(adapters, options = {}) {
7346
7353
  }
7347
7354
  scannedRoots.push(root);
7348
7355
  await walk(root, root, 0);
7356
+ for (const adapter of sourceAdapters) {
7357
+ try {
7358
+ for (const item of await adapter.collect(root, env))
7359
+ addItem(item);
7360
+ } catch (err) {
7361
+ issues.push({
7362
+ path: root,
7363
+ adapter: adapter.id,
7364
+ message: `collect() threw: ${String(err)}`
7365
+ });
7366
+ }
7367
+ }
7349
7368
  }
7350
7369
  return { items, issues, scannedRoots, durationMs: Date.now() - started };
7351
7370
  }
@@ -7396,6 +7415,7 @@ function findProjectRoot(startDir, homeDir) {
7396
7415
  // ../core/dist/chunk.js
7397
7416
  var DEFAULT_MAX_CHARS = 2e3;
7398
7417
  function chunkMarkdown(markdown, maxChars = DEFAULT_MAX_CHARS) {
7418
+ const body = parseFrontmatter(markdown).body;
7399
7419
  const chunks = [];
7400
7420
  const trail = [];
7401
7421
  let buffer = [];
@@ -7405,11 +7425,12 @@ function chunkMarkdown(markdown, maxChars = DEFAULT_MAX_CHARS) {
7405
7425
  buffer = [];
7406
7426
  if (!text)
7407
7427
  return;
7428
+ const headingPath = trail.filter((s) => Boolean(s));
7408
7429
  for (const part of splitLong(text, maxChars)) {
7409
- chunks.push({ index: chunks.length, headingPath: [...trail], text: part });
7430
+ chunks.push({ index: chunks.length, headingPath, text: part });
7410
7431
  }
7411
7432
  };
7412
- for (const line of markdown.split(/\r?\n/)) {
7433
+ for (const line of body.split(/\r?\n/)) {
7413
7434
  if (/^```/.test(line.trim()))
7414
7435
  inFence = !inFence;
7415
7436
  const heading = !inFence && /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
@@ -10053,7 +10074,23 @@ function loadSqlite() {
10053
10074
  }
10054
10075
  var DB_DIR = ".capsa";
10055
10076
  var DB_FILE = "index.db";
10056
- var SCHEMA_VERSION = 3;
10077
+ var SCHEMA_VERSION = 5;
10078
+ var CONTEXT_LOG_DDL = `
10079
+ -- Every context delivery. This is the measurement for the MVP gate and
10080
+ -- the seed of the organisation tier's audit log. Alone among these
10081
+ -- tables its rows are never dropped, so a row has to stand on its own:
10082
+ -- it records what was delivered (path, heading trail, tokens per chunk)
10083
+ -- rather than chunk ids, which a rebuild hands to different text.
10084
+ CREATE TABLE IF NOT EXISTS context_log (
10085
+ id INTEGER PRIMARY KEY,
10086
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
10087
+ source TEXT NOT NULL,
10088
+ query TEXT NOT NULL,
10089
+ delivered TEXT NOT NULL,
10090
+ tokens_est INTEGER NOT NULL,
10091
+ duration_ms INTEGER NOT NULL
10092
+ );
10093
+ `;
10057
10094
  function dbPath(projectRoot) {
10058
10095
  return join6(projectRoot, DB_DIR, DB_FILE);
10059
10096
  }
@@ -10139,20 +10176,32 @@ var Store = class {
10139
10176
  related TEXT
10140
10177
  );
10141
10178
 
10142
- -- Every context delivery. This is the measurement for the MVP gate and
10143
- -- the seed of the organisation tier's audit log.
10144
- CREATE TABLE IF NOT EXISTS context_log (
10145
- id INTEGER PRIMARY KEY,
10146
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
10147
- source TEXT NOT NULL,
10148
- query TEXT NOT NULL,
10149
- chunk_ids TEXT NOT NULL,
10150
- tokens_est INTEGER NOT NULL,
10151
- duration_ms INTEGER NOT NULL
10152
- );
10179
+ ${CONTEXT_LOG_DDL}
10153
10180
  `);
10181
+ this.migrateContextLog();
10154
10182
  this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES ('dimensions', ?)").run(String(this.dimensions));
10155
10183
  }
10184
+ /**
10185
+ * `context_log` outlives every rebuild, so it is the one table that has to
10186
+ * be migrated in place instead of recreated. v5 replaced `chunk_ids` with
10187
+ * `delivered`: a chunk id is a rowid the next `capsa index` reassigns, so an
10188
+ * older row ended up naming whatever text inherited its ids — the measurement
10189
+ * quietly decayed. Rows written before v5 keep everything that still means
10190
+ * something (when, who asked, what for, at what cost) and carry an empty
10191
+ * delivery, because their ids can no longer be resolved honestly.
10192
+ */
10193
+ migrateContextLog() {
10194
+ const columns = this.db.prepare("PRAGMA table_info(context_log)").all();
10195
+ if (columns.some((c) => c.name === "delivered"))
10196
+ return;
10197
+ this.db.exec(`
10198
+ ALTER TABLE context_log RENAME TO context_log_v4;
10199
+ ${CONTEXT_LOG_DDL}
10200
+ INSERT INTO context_log (id, created_at, source, query, delivered, tokens_est, duration_ms)
10201
+ SELECT id, created_at, source, query, '[]', tokens_est, duration_ms FROM context_log_v4;
10202
+ DROP TABLE context_log_v4;
10203
+ `);
10204
+ }
10156
10205
  getItem(path) {
10157
10206
  return this.db.prepare("SELECT * FROM items WHERE path = ?").get(path);
10158
10207
  }
@@ -10235,11 +10284,22 @@ var Store = class {
10235
10284
  return this.db.prepare("SELECT * FROM items ORDER BY kind, rel_path").all();
10236
10285
  }
10237
10286
  logContext(entry) {
10238
- this.db.prepare("INSERT INTO context_log (source, query, chunk_ids, tokens_est, duration_ms) VALUES (?, ?, ?, ?, ?)").run(entry.source, entry.query, JSON.stringify(entry.chunkIds), entry.tokensEst, entry.durationMs);
10287
+ this.db.prepare("INSERT INTO context_log (source, query, delivered, tokens_est, duration_ms) VALUES (?, ?, ?, ?, ?)").run(entry.source, entry.query, JSON.stringify(entry.delivered), entry.tokensEst, entry.durationMs);
10239
10288
  }
10289
+ /**
10290
+ * Returns the stored row, not just its id: `created_at` is SQL's to write,
10291
+ * and the indexed item dates itself from it.
10292
+ */
10240
10293
  recordDecision(title, body, related) {
10241
10294
  const res = this.db.prepare("INSERT INTO decisions (title, body, related) VALUES (?, ?, ?)").run(title, body, related ? JSON.stringify(related) : null);
10242
- return Number(res.lastInsertRowid);
10295
+ return this.decision(Number(res.lastInsertRowid));
10296
+ }
10297
+ decision(id) {
10298
+ return this.db.prepare("SELECT * FROM decisions WHERE id = ?").get(id);
10299
+ }
10300
+ /** Every decision, oldest first. The rows survive `reset()`; the items do not. */
10301
+ listDecisions() {
10302
+ return this.db.prepare("SELECT * FROM decisions ORDER BY id").all();
10243
10303
  }
10244
10304
  close() {
10245
10305
  this.db.close();
@@ -10250,9 +10310,164 @@ var Store = class {
10250
10310
  import { createHash as createHash2 } from "crypto";
10251
10311
  import { relative as relative5 } from "path";
10252
10312
 
10313
+ // ../adapter-git-log/dist/index.js
10314
+ import { execFile } from "child_process";
10315
+ import { existsSync as existsSync2 } from "fs";
10316
+ import { basename as basename4, join as join7 } from "path";
10317
+ import { promisify } from "util";
10318
+ var execFileAsync = promisify(execFile);
10319
+ var DEFAULTS = {
10320
+ limit: 200,
10321
+ maxFiles: 50,
10322
+ maxBodyChars: 4e3,
10323
+ includeMerges: false
10324
+ };
10325
+ var RECORD = "";
10326
+ var UNIT = "";
10327
+ var FORMAT = `${RECORD}%H${UNIT}%h${UNIT}%an${UNIT}%cI${UNIT}%s${UNIT}%b${UNIT}`;
10328
+ var ITEM_DIR = join7(".git", "commits");
10329
+ function gitLogAdapter(options = {}) {
10330
+ const opts = { ...DEFAULTS, ...options };
10331
+ return {
10332
+ id: "git-log",
10333
+ displayName: "Commit",
10334
+ async collect(root, _env) {
10335
+ if (!existsSync2(join7(root, ".git")))
10336
+ return [];
10337
+ const stdout = await runGitLog(root, opts);
10338
+ const items = [];
10339
+ for (const record2 of stdout.split(RECORD)) {
10340
+ const commit = parseRecord(record2);
10341
+ if (commit)
10342
+ items.push(toItem(root, commit, opts));
10343
+ }
10344
+ return items;
10345
+ }
10346
+ };
10347
+ }
10348
+ async function runGitLog(root, opts) {
10349
+ const args = [
10350
+ "--no-pager",
10351
+ "log",
10352
+ `--max-count=${opts.limit}`,
10353
+ "--name-only",
10354
+ "--no-color",
10355
+ `--format=${FORMAT}`
10356
+ ];
10357
+ if (!opts.includeMerges)
10358
+ args.push("--no-merges");
10359
+ try {
10360
+ const { stdout } = await execFileAsync("git", args, {
10361
+ cwd: root,
10362
+ timeout: 3e4,
10363
+ maxBuffer: 32 * 1024 * 1024,
10364
+ windowsHide: true
10365
+ });
10366
+ return stdout;
10367
+ } catch (err) {
10368
+ const e = err;
10369
+ if (/does not have any commits yet|bad default revision/i.test(String(e.stderr ?? ""))) {
10370
+ return "";
10371
+ }
10372
+ if (e.code === "ENOENT") {
10373
+ throw new Error(`${root} has a .git but git is not on PATH; no commits indexed`);
10374
+ }
10375
+ throw new Error(`git log failed in ${root}: ${firstLine2(e.stderr) || String(err)}`);
10376
+ }
10377
+ }
10378
+ function parseRecord(record2) {
10379
+ if (!record2.trim())
10380
+ return void 0;
10381
+ const parts = record2.split(UNIT);
10382
+ const [sha, shortSha, author, date5, subject, body] = parts;
10383
+ if (!sha || !shortSha)
10384
+ return void 0;
10385
+ return {
10386
+ sha,
10387
+ shortSha,
10388
+ author: author ?? "",
10389
+ date: date5 ?? "",
10390
+ subject: subject ?? "",
10391
+ body: (body ?? "").trim(),
10392
+ // Everything past the last field is the file list, one path per line.
10393
+ files: parts.slice(6).join(UNIT).split("\n").map((l) => l.trim()).filter(Boolean)
10394
+ };
10395
+ }
10396
+ function toItem(root, c, opts) {
10397
+ const path = join7(root, ITEM_DIR, `${c.shortSha}.md`);
10398
+ const files = c.files.slice(0, opts.maxFiles);
10399
+ const hidden = c.files.length - files.length;
10400
+ const subject = c.subject || "(no subject)";
10401
+ return {
10402
+ id: stableId(path),
10403
+ format: "git-log",
10404
+ formatLabel: "Commit",
10405
+ name: `${c.shortSha} ${subject}`,
10406
+ // The description becomes the provenance line of every embedded chunk,
10407
+ // so it carries the subject, not the sha.
10408
+ description: subject,
10409
+ path,
10410
+ scope: "project",
10411
+ projectRoot: root,
10412
+ content: renderCommit(c, subject, files, hidden, opts.maxBodyChars),
10413
+ metadata: {
10414
+ sha: c.sha,
10415
+ shortSha: c.shortSha,
10416
+ author: c.author,
10417
+ subject,
10418
+ files: c.files,
10419
+ repo: basename4(root)
10420
+ },
10421
+ knowledge: {
10422
+ kind: "commit",
10423
+ updatedAt: isoDate(c.date),
10424
+ relatedPaths: c.files.length ? c.files : void 0,
10425
+ tags: conventionalTags(subject)
10426
+ }
10427
+ };
10428
+ }
10429
+ function renderCommit(c, subject, files, hidden, maxBodyChars) {
10430
+ const body = c.body.length > maxBodyChars ? `${c.body.slice(0, maxBodyChars)}
10431
+
10432
+ \u2026` : c.body;
10433
+ const lines = [
10434
+ `# ${subject}`,
10435
+ "",
10436
+ `commit \`${c.shortSha}\` \xB7 ${c.author} \xB7 ${c.date.slice(0, 10)}`
10437
+ ];
10438
+ if (body)
10439
+ lines.push("", body);
10440
+ if (files.length) {
10441
+ lines.push("", "## Files", "");
10442
+ for (const f of files)
10443
+ lines.push(`- ${f}`);
10444
+ if (hidden > 0)
10445
+ lines.push(`- \u2026 and ${hidden} more`);
10446
+ }
10447
+ return `${lines.join("\n")}
10448
+ `;
10449
+ }
10450
+ function isoDate(raw2) {
10451
+ if (!raw2)
10452
+ return void 0;
10453
+ const t = Date.parse(raw2);
10454
+ return Number.isNaN(t) ? void 0 : new Date(t).toISOString();
10455
+ }
10456
+ var CONVENTIONAL = /^([a-z]{3,10})(?:\(([^)]{1,40})\))?!?:\s/;
10457
+ function conventionalTags(subject) {
10458
+ const m = CONVENTIONAL.exec(subject.toLowerCase());
10459
+ if (!m)
10460
+ return void 0;
10461
+ const tags = [m[1], ...m[2] ? [m[2]] : []];
10462
+ return tags;
10463
+ }
10464
+ function firstLine2(text) {
10465
+ return (text ?? "").split("\n").find((l) => l.trim()) ?? "";
10466
+ }
10467
+
10253
10468
  // ../adapter-runbooks/dist/index.js
10254
10469
  import { readFile as readFile5, stat as stat3 } from "fs/promises";
10255
- import { basename as basename4, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
10470
+ import { basename as basename5, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
10256
10471
  var DOC_DIRS = /* @__PURE__ */ new Set(["docs", "doc", "runbooks", "runbook", "wiki", "adr", "decisions"]);
10257
10472
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog", "plans", "plan", "roadmap", "milestones", "milestone", "node_modules"]);
10258
10473
  function segments(relPath) {
@@ -10283,7 +10498,7 @@ function runbooksAdapter() {
10283
10498
  const relPathInProject = relative3(projectRoot, entry.absPath);
10284
10499
  const fm = parseFrontmatter(raw2);
10285
10500
  const data = fm.data;
10286
- const title = data["title"] || firstHeading(raw2) || basename4(entry.name, ".md");
10501
+ const title = data["title"] || firstHeading(raw2) || basename5(entry.name, ".md");
10287
10502
  const kindFromPath = segments(relPathInProject).some((p) => ["adr", "decisions"].includes(p.toLowerCase())) ? "decision" : "runbook";
10288
10503
  const tags = parseTags(data["tags"]);
10289
10504
  return [
@@ -10291,7 +10506,7 @@ function runbooksAdapter() {
10291
10506
  id: stableId(entry.absPath),
10292
10507
  format: "runbooks",
10293
10508
  formatLabel: "Runbook",
10294
- name: `${basename4(projectRoot)}${sep4}${relPathInProject}`,
10509
+ name: `${basename5(projectRoot)}${sep4}${relPathInProject}`,
10295
10510
  description: title,
10296
10511
  path: entry.absPath,
10297
10512
  scope: "project",
@@ -10325,7 +10540,7 @@ function firstHeading(raw2) {
10325
10540
 
10326
10541
  // ../adapter-tickets/dist/index.js
10327
10542
  import { readFile as readFile6, stat as stat4 } from "fs/promises";
10328
- import { basename as basename5, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
10543
+ import { basename as basename6, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
10329
10544
  var TICKET_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog"]);
10330
10545
  var PLAN_DIRS = /* @__PURE__ */ new Set(["plans", "plan", "roadmap", "milestones", "milestone"]);
10331
10546
  var CHECKBOX = /^\s*[-*+]\s+\[( |x|X)\]\s+/gm;
@@ -10386,14 +10601,14 @@ function ticketsAdapter() {
10386
10601
  const data = fm.data;
10387
10602
  const derived2 = deriveStatus(raw2);
10388
10603
  const status = normaliseStatus(data["status"]) ?? derived2.status;
10389
- const title = data["title"] || firstHeading2(raw2) || basename5(entry.name, ".md");
10604
+ const title = data["title"] || firstHeading2(raw2) || basename6(entry.name, ".md");
10390
10605
  const tags = parseTags2(data["tags"]);
10391
10606
  return [
10392
10607
  {
10393
10608
  id: stableId(entry.absPath),
10394
10609
  format: "tickets",
10395
10610
  formatLabel: isPlan ? "Plan" : "Ticket",
10396
- name: `${basename5(projectRoot)}${sep5}${relPathInProject}`,
10611
+ name: `${basename6(projectRoot)}${sep5}${relPathInProject}`,
10397
10612
  description: title,
10398
10613
  path: entry.absPath,
10399
10614
  scope: "project",
@@ -10430,6 +10645,66 @@ function firstHeading2(raw2) {
10430
10645
  return void 0;
10431
10646
  }
10432
10647
 
10648
+ // ../index/dist/decision.js
10649
+ import { join as join8 } from "path";
10650
+ var ITEM_DIR2 = join8(".capsa", "decisions");
10651
+ function decisionPath(projectRoot, id) {
10652
+ return join8(projectRoot, ITEM_DIR2, `decision-${id}.md`);
10653
+ }
10654
+ function decisionItem(projectRoot, row) {
10655
+ const path = decisionPath(projectRoot, row.id);
10656
+ const related = parseRelated(row.related);
10657
+ return {
10658
+ id: stableId(path),
10659
+ format: "decision",
10660
+ formatLabel: "Decision",
10661
+ name: row.title,
10662
+ // The description becomes the provenance line of every embedded chunk.
10663
+ description: row.title,
10664
+ path,
10665
+ scope: "project",
10666
+ projectRoot,
10667
+ content: renderDecision(row, related),
10668
+ metadata: { decisionId: row.id, author: row.author, createdAt: row.created_at, related },
10669
+ knowledge: {
10670
+ kind: "decision",
10671
+ // Written once, never edited: the day it was made is the day it last
10672
+ // changed, so recency ranks it from `created_at`.
10673
+ updatedAt: row.created_at,
10674
+ // No lifecycle to report. "unknown" is the neutral weight (1.0) and
10675
+ // says so out loud instead of leaving the column blank.
10676
+ status: "unknown",
10677
+ relatedPaths: related.length ? related : void 0
10678
+ }
10679
+ };
10680
+ }
10681
+ function renderDecision(row, related) {
10682
+ const lines = [
10683
+ `# ${row.title}`,
10684
+ "",
10685
+ `decision \`#${row.id}\` \xB7 ${row.author} \xB7 ${row.created_at.slice(0, 10)}`,
10686
+ "",
10687
+ row.body.trim()
10688
+ ];
10689
+ if (related.length) {
10690
+ lines.push("", "## Related", "");
10691
+ for (const p of related)
10692
+ lines.push(`- ${p}`);
10693
+ }
10694
+ return `${lines.join("\n")}
10695
+ `;
10696
+ }
10697
+ function parseRelated(json2) {
10698
+ if (!json2)
10699
+ return [];
10700
+ try {
10701
+ const parsed = JSON.parse(json2);
10702
+ return Array.isArray(parsed) ? parsed.filter((p) => typeof p === "string") : [];
10703
+ } catch {
10704
+ return [];
10705
+ }
10706
+ }
10707
+
10433
10708
  // ../index/dist/indexer.js
10434
10709
  function knowledgeAdapters() {
10435
10710
  return [
@@ -10437,7 +10712,8 @@ function knowledgeAdapters() {
10437
10712
  claudeSkillsAdapter(),
10438
10713
  cursorRulesAdapter(),
10439
10714
  runbooksAdapter(),
10440
- ticketsAdapter()
10715
+ ticketsAdapter(),
10716
+ gitLogAdapter()
10441
10717
  ];
10442
10718
  }
10443
10719
  function openStore(projectRoot, embedder) {
@@ -10467,26 +10743,15 @@ async function indexProject(projectRoot, embedder, onProgress) {
10467
10743
  }
10468
10744
  if (existing)
10469
10745
  store.deleteItem(existing.id);
10470
- const chunks = chunkMarkdown(item.content);
10471
- const searchTexts = chunks.map((c) => withContext(item, c.headingPath, c.text));
10472
- const vectors = searchTexts.length ? await embedder.embed(searchTexts) : [];
10473
- const k = item.knowledge;
10474
- store.upsertItem({
10475
- id: item.id,
10476
- format: item.format,
10477
- kind: k?.kind ?? "instruction",
10478
- name: item.name,
10479
- path: item.path,
10480
- rel_path: relative5(projectRoot, item.path),
10481
- content_hash: hash2,
10482
- status: k?.status ?? null,
10483
- updated_at: k?.updatedAt ?? null,
10484
- tags: k?.tags ? JSON.stringify(k.tags) : null
10485
- });
10486
- chunks.forEach((c, i) => store.insertChunk(item.id, c.index, c.headingPath, c.text, vectors[i], searchTexts[i]));
10746
+ await indexItem(store, embedder, item, projectRoot, hash2);
10487
10747
  indexed++;
10488
10748
  onProgress?.({ phase: "embed", scanned: items.length, indexed, skipped });
10489
10749
  }
10750
+ const decisions = await indexDecisions(store, embedder, projectRoot);
10751
+ for (const path of decisions.paths)
10752
+ seen.add(path);
10753
+ indexed += decisions.indexed;
10754
+ skipped += decisions.skipped;
10490
10755
  let removed = 0;
10491
10756
  for (const row of store.listItems()) {
10492
10757
  if (!seen.has(row.path)) {
@@ -10495,7 +10760,7 @@ async function indexProject(projectRoot, embedder, onProgress) {
10495
10760
  }
10496
10761
  }
10497
10762
  const out = {
10498
- scanned: items.length,
10763
+ scanned: items.length + decisions.paths.length,
10499
10764
  indexed,
10500
10765
  skipped,
10501
10766
  removed,
@@ -10508,6 +10773,53 @@ async function indexProject(projectRoot, embedder, onProgress) {
10508
10773
  store.close();
10509
10774
  }
10510
10775
  }
10776
+ async function indexDecisions(store, embedder, projectRoot) {
10777
+ const out = { paths: [], indexed: 0, skipped: 0 };
10778
+ for (const row of store.listDecisions()) {
10779
+ const item = decisionItem(projectRoot, row);
10780
+ out.paths.push(item.path);
10781
+ const hash2 = sha1(item.content);
10782
+ const existing = store.getItem(item.path);
10783
+ if (existing && existing.content_hash === hash2) {
10784
+ out.skipped++;
10785
+ continue;
10786
+ }
10787
+ if (existing)
10788
+ store.deleteItem(existing.id);
10789
+ await indexItem(store, embedder, item, projectRoot, hash2);
10790
+ out.indexed++;
10791
+ }
10792
+ return out;
10793
+ }
10794
+ async function recordDecision(store, embedder, projectRoot, input2) {
10795
+ const row = store.recordDecision(input2.title, input2.body, input2.related);
10796
+ const item = decisionItem(projectRoot, row);
10797
+ try {
10798
+ await indexItem(store, embedder, item, projectRoot, sha1(item.content));
10799
+ return { id: row.id, indexed: true };
10800
+ } catch (err) {
10801
+ return { id: row.id, indexed: false, reason: err.message };
10802
+ }
10803
+ }
10804
+ async function indexItem(store, embedder, item, projectRoot, hash2) {
10805
+ const chunks = chunkMarkdown(item.content);
10806
+ const searchTexts = chunks.map((c) => withContext(item, c.headingPath, c.text));
10807
+ const vectors = searchTexts.length ? await embedder.embed(searchTexts) : [];
10808
+ const k = item.knowledge;
10809
+ store.upsertItem({
10810
+ id: item.id,
10811
+ format: item.format,
10812
+ kind: k?.kind ?? "instruction",
10813
+ name: item.name,
10814
+ path: item.path,
10815
+ rel_path: relative5(projectRoot, item.path),
10816
+ content_hash: hash2,
10817
+ status: k?.status ?? null,
10818
+ updated_at: k?.updatedAt ?? null,
10819
+ tags: k?.tags ? JSON.stringify(k.tags) : null
10820
+ });
10821
+ chunks.forEach((c, i) => store.insertChunk(item.id, c.index, c.headingPath, c.text, vectors[i], searchTexts[i]));
10822
+ }
10511
10823
  function withContext(item, headingPath, text) {
10512
10824
  const kind = item.knowledge?.kind ?? "instruction";
10513
10825
  const head = [kind, item.description ?? item.name, ...headingPath].filter(Boolean).join(" \u203A ");
@@ -10720,7 +11032,7 @@ async function retrieve(store, embedder, query, options = {}) {
10720
11032
  store.logContext({
10721
11033
  source: options.source ?? "unknown",
10722
11034
  query,
10723
- chunkIds: chunks.map((c) => c.chunkId),
11035
+ delivered: chunks.map((c) => ({ rel_path: c.relPath, heading_path: c.headingPath, tokens: c.tokens })),
10724
11036
  tokensEst: tokens,
10725
11037
  durationMs
10726
11038
  });
@@ -10769,7 +11081,7 @@ function projectState(store) {
10769
11081
  const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
10770
11082
  const rank = (i) => i.status === "in-progress" ? 0 : 1;
10771
11083
  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 }));
10772
- 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 }));
11084
+ const recentlyChanged = items.filter((i) => i.kind !== "commit").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 }));
10773
11085
  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 }));
10774
11086
  return { counts, open: open2, recentlyChanged, decisions };
10775
11087
  }
@@ -40115,8 +40427,13 @@ _capsa: ${result.chunks.length} chunks, ~${result.tokensEst} tokens, ${result.du
40115
40427
  }, async ({ title, body, related }) => {
40116
40428
  const store = openStore(options.projectRoot, embedder);
40117
40429
  try {
40118
- const id = store.recordDecision(title, body, related);
40119
- return { content: [{ type: "text", text: `Recorded decision #${id}: ${title}` }] };
40430
+ const { id, indexed, reason } = await recordDecision(store, embedder, options.projectRoot, {
40431
+ title,
40432
+ body,
40433
+ related
40434
+ });
40435
+ const note = indexed ? "" : ` \u2014 not searchable yet (${reason}); run \`capsa index\` once that is fixed`;
40436
+ return { content: [{ type: "text", text: `Recorded decision #${id}: ${title}${note}` }] };
40120
40437
  } finally {
40121
40438
  store.close();
40122
40439
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yurtsever/capsa",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.6",
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",