@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.
- package/dist/index.js +370 -53
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
7430
|
+
chunks.push({ index: chunks.length, headingPath, text: part });
|
|
7410
7431
|
}
|
|
7411
7432
|
};
|
|
7412
|
-
for (const line of
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
|
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) ||
|
|
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: `${
|
|
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
|
|
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) ||
|
|
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: `${
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
40119
|
-
|
|
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.
|
|
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",
|