@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.
- package/dist/index.js +179 -37
- 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
|
|
7430
|
+
chunks.push({ index: chunks.length, headingPath, text: part });
|
|
7429
7431
|
}
|
|
7430
7432
|
};
|
|
7431
|
-
for (const line of
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
40294
|
-
|
|
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.
|
|
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",
|