@yurtsever/capsa 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +284 -28
- 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
|
}
|
|
@@ -10227,6 +10246,10 @@ var Store = class {
|
|
|
10227
10246
|
}
|
|
10228
10247
|
}));
|
|
10229
10248
|
}
|
|
10249
|
+
/** All chunks of one item, in document order. */
|
|
10250
|
+
chunksForItem(itemId) {
|
|
10251
|
+
return this.db.prepare("SELECT id, item_id, idx, heading_path, text FROM chunks WHERE item_id = ? ORDER BY idx").all(itemId);
|
|
10252
|
+
}
|
|
10230
10253
|
listItems() {
|
|
10231
10254
|
return this.db.prepare("SELECT * FROM items ORDER BY kind, rel_path").all();
|
|
10232
10255
|
}
|
|
@@ -10246,9 +10269,164 @@ var Store = class {
|
|
|
10246
10269
|
import { createHash as createHash2 } from "crypto";
|
|
10247
10270
|
import { relative as relative5 } from "path";
|
|
10248
10271
|
|
|
10272
|
+
// ../adapter-git-log/dist/index.js
|
|
10273
|
+
import { execFile } from "child_process";
|
|
10274
|
+
import { existsSync as existsSync2 } from "fs";
|
|
10275
|
+
import { basename as basename4, join as join7 } from "path";
|
|
10276
|
+
import { promisify } from "util";
|
|
10277
|
+
var execFileAsync = promisify(execFile);
|
|
10278
|
+
var DEFAULTS = {
|
|
10279
|
+
limit: 200,
|
|
10280
|
+
maxFiles: 50,
|
|
10281
|
+
maxBodyChars: 4e3,
|
|
10282
|
+
includeMerges: false
|
|
10283
|
+
};
|
|
10284
|
+
var RECORD = "";
|
|
10285
|
+
var UNIT = "";
|
|
10286
|
+
var FORMAT = `${RECORD}%H${UNIT}%h${UNIT}%an${UNIT}%cI${UNIT}%s${UNIT}%b${UNIT}`;
|
|
10287
|
+
var ITEM_DIR = join7(".git", "commits");
|
|
10288
|
+
function gitLogAdapter(options = {}) {
|
|
10289
|
+
const opts = { ...DEFAULTS, ...options };
|
|
10290
|
+
return {
|
|
10291
|
+
id: "git-log",
|
|
10292
|
+
displayName: "Commit",
|
|
10293
|
+
async collect(root, _env) {
|
|
10294
|
+
if (!existsSync2(join7(root, ".git")))
|
|
10295
|
+
return [];
|
|
10296
|
+
const stdout = await runGitLog(root, opts);
|
|
10297
|
+
const items = [];
|
|
10298
|
+
for (const record2 of stdout.split(RECORD)) {
|
|
10299
|
+
const commit = parseRecord(record2);
|
|
10300
|
+
if (commit)
|
|
10301
|
+
items.push(toItem(root, commit, opts));
|
|
10302
|
+
}
|
|
10303
|
+
return items;
|
|
10304
|
+
}
|
|
10305
|
+
};
|
|
10306
|
+
}
|
|
10307
|
+
async function runGitLog(root, opts) {
|
|
10308
|
+
const args = [
|
|
10309
|
+
"--no-pager",
|
|
10310
|
+
"log",
|
|
10311
|
+
`--max-count=${opts.limit}`,
|
|
10312
|
+
"--name-only",
|
|
10313
|
+
"--no-color",
|
|
10314
|
+
`--format=${FORMAT}`
|
|
10315
|
+
];
|
|
10316
|
+
if (!opts.includeMerges)
|
|
10317
|
+
args.push("--no-merges");
|
|
10318
|
+
try {
|
|
10319
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
10320
|
+
cwd: root,
|
|
10321
|
+
timeout: 3e4,
|
|
10322
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
10323
|
+
windowsHide: true
|
|
10324
|
+
});
|
|
10325
|
+
return stdout;
|
|
10326
|
+
} catch (err) {
|
|
10327
|
+
const e = err;
|
|
10328
|
+
if (/does not have any commits yet|bad default revision/i.test(String(e.stderr ?? ""))) {
|
|
10329
|
+
return "";
|
|
10330
|
+
}
|
|
10331
|
+
if (e.code === "ENOENT") {
|
|
10332
|
+
throw new Error(`${root} has a .git but git is not on PATH; no commits indexed`);
|
|
10333
|
+
}
|
|
10334
|
+
throw new Error(`git log failed in ${root}: ${firstLine2(e.stderr) || String(err)}`);
|
|
10335
|
+
}
|
|
10336
|
+
}
|
|
10337
|
+
function parseRecord(record2) {
|
|
10338
|
+
if (!record2.trim())
|
|
10339
|
+
return void 0;
|
|
10340
|
+
const parts = record2.split(UNIT);
|
|
10341
|
+
const [sha, shortSha, author, date5, subject, body] = parts;
|
|
10342
|
+
if (!sha || !shortSha)
|
|
10343
|
+
return void 0;
|
|
10344
|
+
return {
|
|
10345
|
+
sha,
|
|
10346
|
+
shortSha,
|
|
10347
|
+
author: author ?? "",
|
|
10348
|
+
date: date5 ?? "",
|
|
10349
|
+
subject: subject ?? "",
|
|
10350
|
+
body: (body ?? "").trim(),
|
|
10351
|
+
// Everything past the last field is the file list, one path per line.
|
|
10352
|
+
files: parts.slice(6).join(UNIT).split("\n").map((l) => l.trim()).filter(Boolean)
|
|
10353
|
+
};
|
|
10354
|
+
}
|
|
10355
|
+
function toItem(root, c, opts) {
|
|
10356
|
+
const path = join7(root, ITEM_DIR, `${c.shortSha}.md`);
|
|
10357
|
+
const files = c.files.slice(0, opts.maxFiles);
|
|
10358
|
+
const hidden = c.files.length - files.length;
|
|
10359
|
+
const subject = c.subject || "(no subject)";
|
|
10360
|
+
return {
|
|
10361
|
+
id: stableId(path),
|
|
10362
|
+
format: "git-log",
|
|
10363
|
+
formatLabel: "Commit",
|
|
10364
|
+
name: `${c.shortSha} ${subject}`,
|
|
10365
|
+
// The description becomes the provenance line of every embedded chunk,
|
|
10366
|
+
// so it carries the subject, not the sha.
|
|
10367
|
+
description: subject,
|
|
10368
|
+
path,
|
|
10369
|
+
scope: "project",
|
|
10370
|
+
projectRoot: root,
|
|
10371
|
+
content: renderCommit(c, subject, files, hidden, opts.maxBodyChars),
|
|
10372
|
+
metadata: {
|
|
10373
|
+
sha: c.sha,
|
|
10374
|
+
shortSha: c.shortSha,
|
|
10375
|
+
author: c.author,
|
|
10376
|
+
subject,
|
|
10377
|
+
files: c.files,
|
|
10378
|
+
repo: basename4(root)
|
|
10379
|
+
},
|
|
10380
|
+
knowledge: {
|
|
10381
|
+
kind: "commit",
|
|
10382
|
+
updatedAt: isoDate(c.date),
|
|
10383
|
+
relatedPaths: c.files.length ? c.files : void 0,
|
|
10384
|
+
tags: conventionalTags(subject)
|
|
10385
|
+
}
|
|
10386
|
+
};
|
|
10387
|
+
}
|
|
10388
|
+
function renderCommit(c, subject, files, hidden, maxBodyChars) {
|
|
10389
|
+
const body = c.body.length > maxBodyChars ? `${c.body.slice(0, maxBodyChars)}
|
|
10390
|
+
|
|
10391
|
+
\u2026` : c.body;
|
|
10392
|
+
const lines = [
|
|
10393
|
+
`# ${subject}`,
|
|
10394
|
+
"",
|
|
10395
|
+
`commit \`${c.shortSha}\` \xB7 ${c.author} \xB7 ${c.date.slice(0, 10)}`
|
|
10396
|
+
];
|
|
10397
|
+
if (body)
|
|
10398
|
+
lines.push("", body);
|
|
10399
|
+
if (files.length) {
|
|
10400
|
+
lines.push("", "## Files", "");
|
|
10401
|
+
for (const f of files)
|
|
10402
|
+
lines.push(`- ${f}`);
|
|
10403
|
+
if (hidden > 0)
|
|
10404
|
+
lines.push(`- \u2026 and ${hidden} more`);
|
|
10405
|
+
}
|
|
10406
|
+
return `${lines.join("\n")}
|
|
10407
|
+
`;
|
|
10408
|
+
}
|
|
10409
|
+
function isoDate(raw2) {
|
|
10410
|
+
if (!raw2)
|
|
10411
|
+
return void 0;
|
|
10412
|
+
const t = Date.parse(raw2);
|
|
10413
|
+
return Number.isNaN(t) ? void 0 : new Date(t).toISOString();
|
|
10414
|
+
}
|
|
10415
|
+
var CONVENTIONAL = /^([a-z]{3,10})(?:\(([^)]{1,40})\))?!?:\s/;
|
|
10416
|
+
function conventionalTags(subject) {
|
|
10417
|
+
const m = CONVENTIONAL.exec(subject.toLowerCase());
|
|
10418
|
+
if (!m)
|
|
10419
|
+
return void 0;
|
|
10420
|
+
const tags = [m[1], ...m[2] ? [m[2]] : []];
|
|
10421
|
+
return tags;
|
|
10422
|
+
}
|
|
10423
|
+
function firstLine2(text) {
|
|
10424
|
+
return (text ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
10425
|
+
}
|
|
10426
|
+
|
|
10249
10427
|
// ../adapter-runbooks/dist/index.js
|
|
10250
10428
|
import { readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
10251
|
-
import { basename as
|
|
10429
|
+
import { basename as basename5, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
|
|
10252
10430
|
var DOC_DIRS = /* @__PURE__ */ new Set(["docs", "doc", "runbooks", "runbook", "wiki", "adr", "decisions"]);
|
|
10253
10431
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog", "plans", "plan", "roadmap", "milestones", "milestone", "node_modules"]);
|
|
10254
10432
|
function segments(relPath) {
|
|
@@ -10279,7 +10457,7 @@ function runbooksAdapter() {
|
|
|
10279
10457
|
const relPathInProject = relative3(projectRoot, entry.absPath);
|
|
10280
10458
|
const fm = parseFrontmatter(raw2);
|
|
10281
10459
|
const data = fm.data;
|
|
10282
|
-
const title = data["title"] || firstHeading(raw2) ||
|
|
10460
|
+
const title = data["title"] || firstHeading(raw2) || basename5(entry.name, ".md");
|
|
10283
10461
|
const kindFromPath = segments(relPathInProject).some((p) => ["adr", "decisions"].includes(p.toLowerCase())) ? "decision" : "runbook";
|
|
10284
10462
|
const tags = parseTags(data["tags"]);
|
|
10285
10463
|
return [
|
|
@@ -10287,7 +10465,7 @@ function runbooksAdapter() {
|
|
|
10287
10465
|
id: stableId(entry.absPath),
|
|
10288
10466
|
format: "runbooks",
|
|
10289
10467
|
formatLabel: "Runbook",
|
|
10290
|
-
name: `${
|
|
10468
|
+
name: `${basename5(projectRoot)}${sep4}${relPathInProject}`,
|
|
10291
10469
|
description: title,
|
|
10292
10470
|
path: entry.absPath,
|
|
10293
10471
|
scope: "project",
|
|
@@ -10321,7 +10499,7 @@ function firstHeading(raw2) {
|
|
|
10321
10499
|
|
|
10322
10500
|
// ../adapter-tickets/dist/index.js
|
|
10323
10501
|
import { readFile as readFile6, stat as stat4 } from "fs/promises";
|
|
10324
|
-
import { basename as
|
|
10502
|
+
import { basename as basename6, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
|
|
10325
10503
|
var TICKET_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog"]);
|
|
10326
10504
|
var PLAN_DIRS = /* @__PURE__ */ new Set(["plans", "plan", "roadmap", "milestones", "milestone"]);
|
|
10327
10505
|
var CHECKBOX = /^\s*[-*+]\s+\[( |x|X)\]\s+/gm;
|
|
@@ -10382,14 +10560,14 @@ function ticketsAdapter() {
|
|
|
10382
10560
|
const data = fm.data;
|
|
10383
10561
|
const derived2 = deriveStatus(raw2);
|
|
10384
10562
|
const status = normaliseStatus(data["status"]) ?? derived2.status;
|
|
10385
|
-
const title = data["title"] || firstHeading2(raw2) ||
|
|
10563
|
+
const title = data["title"] || firstHeading2(raw2) || basename6(entry.name, ".md");
|
|
10386
10564
|
const tags = parseTags2(data["tags"]);
|
|
10387
10565
|
return [
|
|
10388
10566
|
{
|
|
10389
10567
|
id: stableId(entry.absPath),
|
|
10390
10568
|
format: "tickets",
|
|
10391
10569
|
formatLabel: isPlan ? "Plan" : "Ticket",
|
|
10392
|
-
name: `${
|
|
10570
|
+
name: `${basename6(projectRoot)}${sep5}${relPathInProject}`,
|
|
10393
10571
|
description: title,
|
|
10394
10572
|
path: entry.absPath,
|
|
10395
10573
|
scope: "project",
|
|
@@ -10433,7 +10611,8 @@ function knowledgeAdapters() {
|
|
|
10433
10611
|
claudeSkillsAdapter(),
|
|
10434
10612
|
cursorRulesAdapter(),
|
|
10435
10613
|
runbooksAdapter(),
|
|
10436
|
-
ticketsAdapter()
|
|
10614
|
+
ticketsAdapter(),
|
|
10615
|
+
gitLogAdapter()
|
|
10437
10616
|
];
|
|
10438
10617
|
}
|
|
10439
10618
|
function openStore(projectRoot, embedder) {
|
|
@@ -10566,6 +10745,40 @@ function rrf(lists, k = DEFAULT_WEIGHTS.rrfK) {
|
|
|
10566
10745
|
return scores;
|
|
10567
10746
|
}
|
|
10568
10747
|
|
|
10748
|
+
// ../index/dist/pin.js
|
|
10749
|
+
var LEADING_ID = /^([A-Za-z]{1,8}-?\d{1,5}(?:-\d{1,5})?|\d{2,6})(?=[-_.\s]|$)/;
|
|
10750
|
+
function leadingId(relPath) {
|
|
10751
|
+
const base = relPath.split(/[\\/]/).pop() ?? "";
|
|
10752
|
+
return LEADING_ID.exec(base)?.[1];
|
|
10753
|
+
}
|
|
10754
|
+
function pinnedItems(query, items) {
|
|
10755
|
+
const q = query.toLowerCase();
|
|
10756
|
+
const idCount = /* @__PURE__ */ new Map();
|
|
10757
|
+
for (const item of items) {
|
|
10758
|
+
const id = leadingId(item.rel_path)?.toLowerCase();
|
|
10759
|
+
if (id)
|
|
10760
|
+
idCount.set(id, (idCount.get(id) ?? 0) + 1);
|
|
10761
|
+
}
|
|
10762
|
+
const out = [];
|
|
10763
|
+
for (const item of items) {
|
|
10764
|
+
const rel = item.rel_path.toLowerCase();
|
|
10765
|
+
if (q.includes(rel)) {
|
|
10766
|
+
out.push(item);
|
|
10767
|
+
continue;
|
|
10768
|
+
}
|
|
10769
|
+
const id = leadingId(item.rel_path);
|
|
10770
|
+
if (!id || (idCount.get(id.toLowerCase()) ?? 0) > 1)
|
|
10771
|
+
continue;
|
|
10772
|
+
const re = new RegExp(`(^|[^\\w-])${escape2(id)}(?=$|[^\\w-])`, "i");
|
|
10773
|
+
if (re.test(query))
|
|
10774
|
+
out.push(item);
|
|
10775
|
+
}
|
|
10776
|
+
return out;
|
|
10777
|
+
}
|
|
10778
|
+
function escape2(s) {
|
|
10779
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10780
|
+
}
|
|
10781
|
+
|
|
10569
10782
|
// ../index/dist/retrieve.js
|
|
10570
10783
|
async function retrieve(store, embedder, query, options = {}) {
|
|
10571
10784
|
const started = Date.now();
|
|
@@ -10573,31 +10786,73 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10573
10786
|
const candidates = options.candidates ?? 30;
|
|
10574
10787
|
const weights = options.weights ?? DEFAULT_WEIGHTS;
|
|
10575
10788
|
const minRelative = options.minRelativeScore ?? 0.5;
|
|
10789
|
+
const perItem = /* @__PURE__ */ new Map();
|
|
10790
|
+
const chunks = [];
|
|
10791
|
+
const debug = [];
|
|
10792
|
+
let tokens = 0;
|
|
10793
|
+
const pinned = pinnedItems(query, store.listItems());
|
|
10794
|
+
for (const item of pinned) {
|
|
10795
|
+
for (const c of store.chunksForItem(item.id)) {
|
|
10796
|
+
const t = estimateTokens(c.text);
|
|
10797
|
+
const fits = tokens + t <= maxTokens || chunks.length === 0;
|
|
10798
|
+
if (options.debug) {
|
|
10799
|
+
debug.push({
|
|
10800
|
+
chunkId: c.id,
|
|
10801
|
+
relPath: item.rel_path,
|
|
10802
|
+
headingPath: JSON.parse(c.heading_path),
|
|
10803
|
+
kind: item.kind,
|
|
10804
|
+
status: item.status,
|
|
10805
|
+
vecRank: null,
|
|
10806
|
+
ftsRank: null,
|
|
10807
|
+
fused: 0,
|
|
10808
|
+
factor: 1,
|
|
10809
|
+
score: Number.POSITIVE_INFINITY,
|
|
10810
|
+
kept: fits,
|
|
10811
|
+
why: fits ? "pinned" : "budget"
|
|
10812
|
+
});
|
|
10813
|
+
}
|
|
10814
|
+
if (!fits)
|
|
10815
|
+
continue;
|
|
10816
|
+
perItem.set(item.id, (perItem.get(item.id) ?? 0) + 1);
|
|
10817
|
+
tokens += t;
|
|
10818
|
+
chunks.push({
|
|
10819
|
+
chunkId: c.id,
|
|
10820
|
+
score: Number.POSITIVE_INFINITY,
|
|
10821
|
+
kind: item.kind,
|
|
10822
|
+
status: item.status,
|
|
10823
|
+
updatedAt: item.updated_at,
|
|
10824
|
+
relPath: item.rel_path,
|
|
10825
|
+
headingPath: JSON.parse(c.heading_path),
|
|
10826
|
+
text: c.text,
|
|
10827
|
+
tokens: t
|
|
10828
|
+
});
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
const pinnedIds = new Set(pinned.map((i) => i.id));
|
|
10832
|
+
const neighbourCutoff = pinned.length ? Math.max(minRelative, 0.75) : minRelative;
|
|
10833
|
+
const neighbourBudget = pinned.length ? Math.min(maxTokens, tokens + Math.floor(maxTokens / 3)) : maxTokens;
|
|
10576
10834
|
const [qv] = await embedder.embed([query]);
|
|
10577
10835
|
const vec = qv ? store.vectorSearch(qv, candidates).map((r) => r.chunkId) : [];
|
|
10578
10836
|
const fts = store.textSearch(query, candidates).map((r) => r.chunkId);
|
|
10579
10837
|
const fused = rrf([vec, fts], weights.rrfK);
|
|
10580
|
-
const rows = store.chunksWithItems([...fused.keys()]);
|
|
10838
|
+
const rows = store.chunksWithItems([...fused.keys()]).filter((r) => !pinnedIds.has(r.item_id));
|
|
10581
10839
|
const now = Date.now();
|
|
10582
10840
|
const scored = rows.map((r) => {
|
|
10583
10841
|
const fusedScore = fused.get(r.id) ?? 0;
|
|
10584
10842
|
const factor = signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now);
|
|
10585
10843
|
return { row: r, fusedScore, factor, score: fusedScore * factor };
|
|
10586
10844
|
}).sort((a, b) => b.score - a.score);
|
|
10587
|
-
const perItem = /* @__PURE__ */ new Map();
|
|
10588
|
-
const chunks = [];
|
|
10589
|
-
const debug = [];
|
|
10590
|
-
let tokens = 0;
|
|
10591
10845
|
const best = scored[0]?.score ?? 0;
|
|
10592
|
-
let cut =
|
|
10846
|
+
let cut = tokens >= maxTokens;
|
|
10847
|
+
let kept = 0;
|
|
10593
10848
|
for (const { row, fusedScore, factor, score } of scored) {
|
|
10594
10849
|
let why = "kept";
|
|
10595
|
-
if (cut ||
|
|
10850
|
+
if (cut || kept > 0 && score < best * neighbourCutoff) {
|
|
10596
10851
|
cut = true;
|
|
10597
10852
|
why = "cutoff";
|
|
10598
10853
|
} else if ((perItem.get(row.item_id) ?? 0) >= 2) {
|
|
10599
10854
|
why = "per-item cap";
|
|
10600
|
-
} else if (tokens + estimateTokens(row.text) >
|
|
10855
|
+
} else if (tokens + estimateTokens(row.text) > neighbourBudget && chunks.length > 0) {
|
|
10601
10856
|
why = "budget";
|
|
10602
10857
|
}
|
|
10603
10858
|
if (options.debug) {
|
|
@@ -10621,6 +10876,7 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10621
10876
|
const t = estimateTokens(row.text);
|
|
10622
10877
|
perItem.set(row.item_id, (perItem.get(row.item_id) ?? 0) + 1);
|
|
10623
10878
|
tokens += t;
|
|
10879
|
+
kept++;
|
|
10624
10880
|
chunks.push({
|
|
10625
10881
|
chunkId: row.id,
|
|
10626
10882
|
score,
|
|
@@ -10632,7 +10888,7 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10632
10888
|
text: row.text,
|
|
10633
10889
|
tokens: t
|
|
10634
10890
|
});
|
|
10635
|
-
if (tokens >=
|
|
10891
|
+
if (tokens >= neighbourBudget)
|
|
10636
10892
|
cut = true;
|
|
10637
10893
|
}
|
|
10638
10894
|
const durationMs = Date.now() - started;
|
|
@@ -10652,15 +10908,15 @@ function rankIn(list, id) {
|
|
|
10652
10908
|
function formatDebug(result) {
|
|
10653
10909
|
if (!result.debug)
|
|
10654
10910
|
return "";
|
|
10655
|
-
const lines = [" vec fts factor score keep
|
|
10911
|
+
const lines = [" vec fts factor score keep where"];
|
|
10656
10912
|
for (const d of result.debug) {
|
|
10657
10913
|
const where = [d.relPath, ...d.headingPath].join(" \u203A ").slice(0, 70);
|
|
10658
10914
|
lines.push([
|
|
10659
10915
|
String(d.vecRank ?? "-").padStart(5),
|
|
10660
10916
|
String(d.ftsRank ?? "-").padStart(4),
|
|
10661
10917
|
d.factor.toFixed(2).padStart(7),
|
|
10662
|
-
d.score.toFixed(4).padStart(8),
|
|
10663
|
-
(d.kept ? "yes" : d.why).padEnd(12),
|
|
10918
|
+
(Number.isFinite(d.score) ? d.score.toFixed(4) : "pin").padStart(8),
|
|
10919
|
+
(d.kept ? d.why === "pinned" ? "pinned" : "yes" : d.why).padEnd(12),
|
|
10664
10920
|
where
|
|
10665
10921
|
].join(" "));
|
|
10666
10922
|
}
|
|
@@ -10688,7 +10944,7 @@ function projectState(store) {
|
|
|
10688
10944
|
const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
|
|
10689
10945
|
const rank = (i) => i.status === "in-progress" ? 0 : 1;
|
|
10690
10946
|
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 }));
|
|
10691
|
-
const recentlyChanged =
|
|
10947
|
+
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 }));
|
|
10692
10948
|
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 }));
|
|
10693
10949
|
return { counts, open: open2, recentlyChanged, decisions };
|
|
10694
10950
|
}
|
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.5",
|
|
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",
|