sphica 0.6.0 → 0.6.2

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/mcp.js CHANGED
@@ -46233,34 +46233,81 @@ async function checkFindings(db, projectId2, files, raw) {
46233
46233
  }
46234
46234
 
46235
46235
  // server/src/search.ts
46236
- var POOL = 200;
46236
+ var UNIT_PAGE = 200;
46237
+ var UNIT_SCAN_MAX = 2000;
46238
+ var SOURCE_PAGE = 50;
46239
+ var SOURCE_SCAN_MAX = 600;
46240
+ var SOURCE_SCAN_BYTES = 64 * 1024 * 1024;
46237
46241
  var LIFE_ORDER = { active: 0, candidate: 1, superseded: 2, withdrawn: 3 };
46238
46242
  var strong = (matched, of) => matched * 2 > of;
46239
46243
  async function searchUnits(db, projectId2, q) {
46240
46244
  const wanted = queryTerms(q.question);
46241
46245
  const match = ftsQuery(q.question);
46242
46246
  if (!match)
46243
- return { hits: [], weaker: 0, terms: wanted };
46244
- let query = db.selectFrom(sql`(select unit_fts.rowid as rowid, bm25(unit_fts, 3, 2, 1) as rank from unit_fts join unit on unit.id = unit_fts.rowid where unit_fts match ${match} and unit.project_id = ${projectId2})`.as("f")).innerJoin("unit as u", "u.id", "f.rowid").where("u.project_id", "=", projectId2).where("u.extraction", "=", "supported").select([
46245
- "u.id",
46246
- "u.key",
46247
- "u.kind",
46248
- "u.stance",
46249
- "u.lifecycle",
46250
- "u.text",
46251
- "u.why",
46252
- "u.scope_note",
46253
- "u.revisit_when",
46254
- "u.content_hash",
46255
- "f.rank"
46256
- ]);
46247
+ return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0 };
46248
+ let query = db.selectFrom(sql`(select unit_fts.rowid as rowid, bm25(unit_fts, 3, 2, 1) as rank from unit_fts join unit on unit.id = unit_fts.rowid where unit_fts match ${match} and unit.project_id = ${projectId2})`.as("f")).innerJoin("unit as u", "u.id", "f.rowid").where("u.project_id", "=", projectId2).where("u.extraction", "=", "supported");
46257
46249
  if (q.kinds?.length)
46258
46250
  query = query.where("u.kind", "in", q.kinds);
46259
46251
  if (q.lifecycles?.length)
46260
46252
  query = query.where("u.lifecycle", "in", q.lifecycles);
46261
46253
  if (q.path)
46262
46254
  query = query.where(({ exists, selectFrom }) => exists(selectFrom("unit_anchor as a").select("a.id").whereRef("a.unit_id", "=", "u.id").where("a.path", "=", q.path ?? "").where("a.retired_at", "is", null)));
46263
- const rows = await query.orderBy("f.rank").limit(POOL).execute();
46255
+ const ranked = await query.select(["u.id", "f.rank"]).orderBy("f.rank").orderBy("u.id").limit(UNIT_SCAN_MAX + 1).execute();
46256
+ const stopped = ranked.length > UNIT_SCAN_MAX;
46257
+ let weaker = 0;
46258
+ let read = 0;
46259
+ const hits = [];
46260
+ for (let at = 0;at < Math.min(ranked.length, UNIT_SCAN_MAX); at += UNIT_PAGE) {
46261
+ const part2 = ranked.slice(at, Math.min(at + UNIT_PAGE, UNIT_SCAN_MAX));
46262
+ const order = new Map(part2.map((r, i) => [r.id, i]));
46263
+ const rows = (await query.select([
46264
+ "u.id",
46265
+ "u.key",
46266
+ "u.kind",
46267
+ "u.stance",
46268
+ "u.lifecycle",
46269
+ "u.text",
46270
+ "u.why",
46271
+ "u.scope_note",
46272
+ "u.revisit_when",
46273
+ "u.content_hash",
46274
+ "f.rank"
46275
+ ]).where("u.id", "in", part2.map((r) => r.id)).execute()).sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
46276
+ read += rows.length;
46277
+ weaker += await judgeUnits(db, rows, wanted, hits);
46278
+ }
46279
+ const shown = new Set(hits.map((h) => h.id));
46280
+ for (const h of [...hits].filter((x) => x.lifecycle === "superseded")) {
46281
+ let successors = db.selectFrom("unit_link as l").innerJoin("unit as n", "n.id", "l.from_unit").where("l.to_unit", "=", h.id).where("l.kind", "=", "supersedes").where("n.extraction", "=", "supported");
46282
+ if (q.kinds?.length)
46283
+ successors = successors.where("n.kind", "in", q.kinds);
46284
+ if (q.lifecycles?.length)
46285
+ successors = successors.where("n.lifecycle", "in", q.lifecycles);
46286
+ const next = await successors.select(["n.id", "n.key", "n.kind", "n.stance", "n.lifecycle", "n.text", "n.why", "n.revisit_when"]).execute();
46287
+ for (const n of next)
46288
+ if (!shown.has(n.id)) {
46289
+ shown.add(n.id);
46290
+ hits.push({
46291
+ ...n,
46292
+ options: [],
46293
+ anchors: [],
46294
+ matched: [],
46295
+ aliasOnly: false,
46296
+ successorOf: h.key,
46297
+ rank: h.rank
46298
+ });
46299
+ }
46300
+ }
46301
+ hits.sort((a, b) => (LIFE_ORDER[a.lifecycle] ?? 9) - (LIFE_ORDER[b.lifecycle] ?? 9) || b.matched.length - a.matched.length || a.rank - b.rank);
46302
+ return {
46303
+ hits: hits.slice(0, q.limit).map(({ rank: _rank, ...h }) => h),
46304
+ weaker,
46305
+ terms: wanted,
46306
+ stopped,
46307
+ read
46308
+ };
46309
+ }
46310
+ async function judgeUnits(db, rows, wanted, hits) {
46264
46311
  const ids = rows.map((r) => r.id);
46265
46312
  const [options, anchors, aliases] = ids.length ? await Promise.all([
46266
46313
  db.selectFrom("unit_option").select(["unit_id", "text", "outcome", "why"]).where("unit_id", "in", ids).orderBy("position").execute(),
@@ -46268,7 +46315,6 @@ async function searchUnits(db, projectId2, q) {
46268
46315
  db.selectFrom("unit_alias").select(["unit_id", "terms", "content_hash"]).where("unit_id", "in", ids).orderBy("id", "desc").execute()
46269
46316
  ]) : [[], [], []];
46270
46317
  let weaker = 0;
46271
- const hits = [];
46272
46318
  for (const r of rows) {
46273
46319
  const opts = options.filter((o) => o.unit_id === r.id);
46274
46320
  const anch = anchors.filter((a) => a.unit_id === r.id);
@@ -46305,69 +46351,63 @@ async function searchUnits(db, projectId2, q) {
46305
46351
  rank: r.rank
46306
46352
  });
46307
46353
  }
46308
- const shown = new Set(hits.map((h) => h.id));
46309
- for (const h of [...hits].filter((x) => x.lifecycle === "superseded")) {
46310
- let successors = db.selectFrom("unit_link as l").innerJoin("unit as n", "n.id", "l.from_unit").where("l.to_unit", "=", h.id).where("l.kind", "=", "supersedes").where("n.extraction", "=", "supported");
46311
- if (q.kinds?.length)
46312
- successors = successors.where("n.kind", "in", q.kinds);
46313
- if (q.lifecycles?.length)
46314
- successors = successors.where("n.lifecycle", "in", q.lifecycles);
46315
- const next = await successors.select(["n.id", "n.key", "n.kind", "n.stance", "n.lifecycle", "n.text", "n.why", "n.revisit_when"]).execute();
46316
- for (const n of next)
46317
- if (!shown.has(n.id)) {
46318
- shown.add(n.id);
46319
- hits.push({
46320
- ...n,
46321
- options: [],
46322
- anchors: [],
46323
- matched: [],
46324
- aliasOnly: false,
46325
- successorOf: h.key,
46326
- rank: h.rank
46327
- });
46328
- }
46329
- }
46330
- hits.sort((a, b) => (LIFE_ORDER[a.lifecycle] ?? 9) - (LIFE_ORDER[b.lifecycle] ?? 9) || b.matched.length - a.matched.length || a.rank - b.rank);
46331
- return { hits: hits.slice(0, q.limit).map(({ rank: _rank, ...h }) => h), weaker, terms: wanted };
46354
+ return weaker;
46332
46355
  }
46333
46356
  async function searchSources(db, projectId2, question, limit) {
46334
46357
  const wanted = queryTerms(question);
46335
46358
  const match = ftsQuery(question);
46336
46359
  if (!match)
46337
- return { hits: [], weaker: 0, terms: wanted };
46338
- const rows = await db.selectFrom(sql`(select source_fts.rowid as rowid, bm25(source_fts) as rank from source_fts join source on source.id = source_fts.rowid where source_fts match ${match} and source.project_id = ${projectId2})`.as("f")).innerJoin("source as s", "s.id", "f.rowid").where("s.project_id", "=", projectId2).select([
46339
- "s.id",
46340
- "s.kind",
46341
- "s.artifact",
46342
- "s.author_kind",
46343
- "s.author_login",
46344
- "s.created_at",
46345
- "s.text",
46346
- "f.rank"
46347
- ]).orderBy("f.rank").limit(POOL).execute();
46360
+ return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0 };
46361
+ const query = db.selectFrom(sql`(select source_fts.rowid as rowid, bm25(source_fts) as rank from source_fts join source on source.id = source_fts.rowid where source_fts match ${match} and source.project_id = ${projectId2})`.as("f")).innerJoin("source as s", "s.id", "f.rowid").where("s.project_id", "=", projectId2);
46362
+ const ranked = await query.select(["s.id", "f.rank"]).orderBy("f.rank").orderBy("s.id").limit(SOURCE_SCAN_MAX + 1).execute();
46348
46363
  let weaker = 0;
46364
+ let stopped = false;
46349
46365
  const hits = [];
46350
- for (const r of rows) {
46351
- const own2 = new Set(terms(r.text));
46352
- const matched = wanted.filter((w) => own2.has(w));
46353
- if (!strong(matched.length, wanted.length)) {
46354
- weaker++;
46355
- continue;
46366
+ let read = 0;
46367
+ let bytes2 = 0;
46368
+ scan:
46369
+ for (let at = 0;at < ranked.length; at += SOURCE_PAGE) {
46370
+ const ids = ranked.slice(at, at + SOURCE_PAGE).map((r) => r.id);
46371
+ const byId = new Map((await db.selectFrom("source").select(["id", "kind", "artifact", "author_kind", "author_login", "created_at", "text"]).where("id", "in", ids).execute()).map((r) => [r.id, r]));
46372
+ for (const id of ids) {
46373
+ if (read >= SOURCE_SCAN_MAX || bytes2 >= SOURCE_SCAN_BYTES) {
46374
+ stopped = true;
46375
+ break scan;
46376
+ }
46377
+ const r = byId.get(id);
46378
+ if (!r)
46379
+ continue;
46380
+ read++;
46381
+ bytes2 += Buffer.byteLength(r.text);
46382
+ const own2 = new Set(terms(r.text));
46383
+ const matched = wanted.filter((w) => own2.has(w));
46384
+ if (!strong(matched.length, wanted.length)) {
46385
+ weaker++;
46386
+ continue;
46387
+ }
46388
+ hits.push({
46389
+ id: r.id,
46390
+ kind: r.kind,
46391
+ artifact: r.artifact,
46392
+ author: r.author_login ?? r.author_kind,
46393
+ created_at: r.created_at,
46394
+ text: r.text,
46395
+ matched
46396
+ });
46397
+ if (hits.length >= limit)
46398
+ break scan;
46399
+ }
46356
46400
  }
46357
- hits.push({
46358
- id: r.id,
46359
- kind: r.kind,
46360
- artifact: r.artifact,
46361
- author: r.author_login ?? r.author_kind,
46362
- created_at: r.created_at,
46363
- text: r.text,
46364
- matched
46365
- });
46366
- }
46367
- return { hits: hits.slice(0, limit), weaker, terms: wanted };
46401
+ if (!stopped && hits.length < limit && ranked.length > SOURCE_SCAN_MAX)
46402
+ stopped = true;
46403
+ return { hits, weaker, terms: wanted, stopped, read };
46368
46404
  }
46369
46405
 
46370
46406
  // server/src/status.ts
46407
+ async function pendingCount(db, projectId2) {
46408
+ const r = await db.selectFrom("session as s").where("s.project_id", "=", projectId2).where((eb) => eb.exists(eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id").where("m.author_kind", "=", "owner").where((eb2) => eb2.not(eb2.exists(eb2.selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x"))))).select(sql`1`.as("x")))).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst();
46409
+ return Number(r?.n ?? 0);
46410
+ }
46371
46411
  async function coverage(db, projectId2) {
46372
46412
  const count = async (q) => Number((await q)?.n ?? 0);
46373
46413
  const units = (where) => count(where(unitBase()).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst());
@@ -46375,7 +46415,7 @@ async function coverage(db, projectId2) {
46375
46415
  const processed = (outcome) => db.selectFrom("source_processing as p").innerJoin("source as m", "m.id", "p.source_id").where("m.project_id", "=", projectId2).where("p.outcome", "=", outcome).where("m.session_id", "is not", null).select("m.session_id");
46376
46416
  const [sessions, pendingSessions, emptySessions, sources, active, candidates, quarantined, work] = await Promise.all([
46377
46417
  count(db.selectFrom("session").where("project_id", "=", projectId2).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
46378
- count(db.selectFrom("session as s").where("s.project_id", "=", projectId2).where((eb) => eb.exists(eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id").where("m.author_kind", "=", "owner").where((eb2) => eb2.not(eb2.exists(eb2.selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x"))))).select(sql`1`.as("x")))).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
46418
+ pendingCount(db, projectId2),
46379
46419
  count(db.selectFrom("session").where("id", "in", processed("no_unit")).where("id", "not in", processed("units")).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
46380
46420
  count(db.selectFrom("source").where("project_id", "=", projectId2).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
46381
46421
  units((q) => q.where("lifecycle", "=", "active")),
@@ -46418,6 +46458,10 @@ async function projectOf(cwd) {
46418
46458
  return `${head(inline(place.name), 200)} is not registered with Sphica (run \`sphica init\` there).`;
46419
46459
  return { id, root: place.root };
46420
46460
  }
46461
+ var among = (r) => r.stopped ? `among the first ${r.read} candidates by rank ` : "";
46462
+ var stoppedAfter = (r) => r.stopped ? `
46463
+
46464
+ Stopped after ${r.read} candidates by rank; more may match.` : "";
46421
46465
  var hitText = (h) => [
46422
46466
  `## ${h.key} (u${h.id}): ${h.kind}${h.stance ? ` ${h.stance}` : ""}, ${h.lifecycle}`,
46423
46467
  head(h.text, 600),
@@ -46484,12 +46528,12 @@ server.registerTool("search", {
46484
46528
  if (a.sources) {
46485
46529
  const r2 = await searchSources(db, p.id, a.query, limit);
46486
46530
  if (!r2.hits.length)
46487
- return text(`No source holds most of: ${r2.terms.join(", ") || "(no searchable words)"}. ${r2.weaker} weaker matches left out.`);
46531
+ return text(`No source ${among(r2)}holds most of: ${r2.terms.join(", ") || "(no searchable words)"}. ${r2.weaker} weaker matches left out.${r2.stopped ? " Search with more specific words." : ""}`);
46488
46532
  return text(framed(r2.hits.map((h) => `## s${h.id}: ${h.kind} ${h.artifact}, ${h.author}, ${h.created_at}
46489
46533
  ${head(h.text, 800)}
46490
46534
  Matched: ${h.matched.join(", ")}`).join(`
46491
46535
 
46492
- `)));
46536
+ `).concat(stoppedAfter(r2))));
46493
46537
  }
46494
46538
  const r = await searchUnits(db, p.id, {
46495
46539
  question: a.query,
@@ -46499,10 +46543,10 @@ Matched: ${h.matched.join(", ")}`).join(`
46499
46543
  limit
46500
46544
  });
46501
46545
  if (!r.hits.length)
46502
- return text(`No record holds most of: ${r.terms.join(", ") || "(no searchable words)"}. ${r.weaker} weaker matches left out. ` + "Search again with other words or the other language, or search sources; status says whether sessions are still untraced.");
46546
+ return text(`No record ${among(r)}holds most of: ${r.terms.join(", ") || "(no searchable words)"}. ${r.weaker} weaker matches left out. ` + (r.stopped ? "Search with more specific words. " : "") + "Search again with other words or the other language, or search sources; status says whether sessions are still untraced.");
46503
46547
  return text(framed(`${r.hits.map(hitText).join(`
46504
46548
 
46505
- `)}
46549
+ `)}${stoppedAfter(r)}
46506
46550
 
46507
46551
  Read a record by its key before relying on it.`));
46508
46552
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
5
5
  "license": "MIT",
6
6
  "type": "module",