@yurtsever/capsa 0.1.0-alpha.5 → 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 +179 -37
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7415,6 +7415,7 @@ function findProjectRoot(startDir, homeDir) {
7415
7415
  // ../core/dist/chunk.js
7416
7416
  var DEFAULT_MAX_CHARS = 2e3;
7417
7417
  function chunkMarkdown(markdown, maxChars = DEFAULT_MAX_CHARS) {
7418
+ const body = parseFrontmatter(markdown).body;
7418
7419
  const chunks = [];
7419
7420
  const trail = [];
7420
7421
  let buffer = [];
@@ -7424,11 +7425,12 @@ function chunkMarkdown(markdown, maxChars = DEFAULT_MAX_CHARS) {
7424
7425
  buffer = [];
7425
7426
  if (!text)
7426
7427
  return;
7428
+ const headingPath = trail.filter((s) => Boolean(s));
7427
7429
  for (const part of splitLong(text, maxChars)) {
7428
- chunks.push({ index: chunks.length, headingPath: [...trail], text: part });
7430
+ chunks.push({ index: chunks.length, headingPath, text: part });
7429
7431
  }
7430
7432
  };
7431
- for (const line of markdown.split(/\r?\n/)) {
7433
+ for (const line of body.split(/\r?\n/)) {
7432
7434
  if (/^```/.test(line.trim()))
7433
7435
  inFence = !inFence;
7434
7436
  const heading = !inFence && /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
@@ -10072,7 +10074,23 @@ function loadSqlite() {
10072
10074
  }
10073
10075
  var DB_DIR = ".capsa";
10074
10076
  var DB_FILE = "index.db";
10075
- 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
+ `;
10076
10094
  function dbPath(projectRoot) {
10077
10095
  return join6(projectRoot, DB_DIR, DB_FILE);
10078
10096
  }
@@ -10158,20 +10176,32 @@ var Store = class {
10158
10176
  related TEXT
10159
10177
  );
10160
10178
 
10161
- -- Every context delivery. This is the measurement for the MVP gate and
10162
- -- the seed of the organisation tier's audit log.
10163
- CREATE TABLE IF NOT EXISTS context_log (
10164
- id INTEGER PRIMARY KEY,
10165
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
10166
- source TEXT NOT NULL,
10167
- query TEXT NOT NULL,
10168
- chunk_ids TEXT NOT NULL,
10169
- tokens_est INTEGER NOT NULL,
10170
- duration_ms INTEGER NOT NULL
10171
- );
10179
+ ${CONTEXT_LOG_DDL}
10172
10180
  `);
10181
+ this.migrateContextLog();
10173
10182
  this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES ('dimensions', ?)").run(String(this.dimensions));
10174
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
+ }
10175
10205
  getItem(path) {
10176
10206
  return this.db.prepare("SELECT * FROM items WHERE path = ?").get(path);
10177
10207
  }
@@ -10254,11 +10284,22 @@ var Store = class {
10254
10284
  return this.db.prepare("SELECT * FROM items ORDER BY kind, rel_path").all();
10255
10285
  }
10256
10286
  logContext(entry) {
10257
- 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);
10258
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
+ */
10259
10293
  recordDecision(title, body, related) {
10260
10294
  const res = this.db.prepare("INSERT INTO decisions (title, body, related) VALUES (?, ?, ?)").run(title, body, related ? JSON.stringify(related) : null);
10261
- 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();
10262
10303
  }
10263
10304
  close() {
10264
10305
  this.db.close();
@@ -10604,6 +10645,66 @@ function firstHeading2(raw2) {
10604
10645
  return void 0;
10605
10646
  }
10606
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
+
10607
10708
  // ../index/dist/indexer.js
10608
10709
  function knowledgeAdapters() {
10609
10710
  return [
@@ -10642,26 +10743,15 @@ async function indexProject(projectRoot, embedder, onProgress) {
10642
10743
  }
10643
10744
  if (existing)
10644
10745
  store.deleteItem(existing.id);
10645
- const chunks = chunkMarkdown(item.content);
10646
- const searchTexts = chunks.map((c) => withContext(item, c.headingPath, c.text));
10647
- const vectors = searchTexts.length ? await embedder.embed(searchTexts) : [];
10648
- const k = item.knowledge;
10649
- store.upsertItem({
10650
- id: item.id,
10651
- format: item.format,
10652
- kind: k?.kind ?? "instruction",
10653
- name: item.name,
10654
- path: item.path,
10655
- rel_path: relative5(projectRoot, item.path),
10656
- content_hash: hash2,
10657
- status: k?.status ?? null,
10658
- updated_at: k?.updatedAt ?? null,
10659
- tags: k?.tags ? JSON.stringify(k.tags) : null
10660
- });
10661
- 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);
10662
10747
  indexed++;
10663
10748
  onProgress?.({ phase: "embed", scanned: items.length, indexed, skipped });
10664
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;
10665
10755
  let removed = 0;
10666
10756
  for (const row of store.listItems()) {
10667
10757
  if (!seen.has(row.path)) {
@@ -10670,7 +10760,7 @@ async function indexProject(projectRoot, embedder, onProgress) {
10670
10760
  }
10671
10761
  }
10672
10762
  const out = {
10673
- scanned: items.length,
10763
+ scanned: items.length + decisions.paths.length,
10674
10764
  indexed,
10675
10765
  skipped,
10676
10766
  removed,
@@ -10683,6 +10773,53 @@ async function indexProject(projectRoot, embedder, onProgress) {
10683
10773
  store.close();
10684
10774
  }
10685
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
+ }
10686
10823
  function withContext(item, headingPath, text) {
10687
10824
  const kind = item.knowledge?.kind ?? "instruction";
10688
10825
  const head = [kind, item.description ?? item.name, ...headingPath].filter(Boolean).join(" \u203A ");
@@ -10895,7 +11032,7 @@ async function retrieve(store, embedder, query, options = {}) {
10895
11032
  store.logContext({
10896
11033
  source: options.source ?? "unknown",
10897
11034
  query,
10898
- chunkIds: chunks.map((c) => c.chunkId),
11035
+ delivered: chunks.map((c) => ({ rel_path: c.relPath, heading_path: c.headingPath, tokens: c.tokens })),
10899
11036
  tokensEst: tokens,
10900
11037
  durationMs
10901
11038
  });
@@ -40290,8 +40427,13 @@ _capsa: ${result.chunks.length} chunks, ~${result.tokensEst} tokens, ${result.du
40290
40427
  }, async ({ title, body, related }) => {
40291
40428
  const store = openStore(options.projectRoot, embedder);
40292
40429
  try {
40293
- const id = store.recordDecision(title, body, related);
40294
- 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}` }] };
40295
40437
  } finally {
40296
40438
  store.close();
40297
40439
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yurtsever/capsa",
3
- "version": "0.1.0-alpha.5",
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",