sphica 0.6.1 → 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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/db/schema.sql +1 -0
- package/dist/deliver.js +64 -20
- package/dist/mcp.js +118 -74
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "sphica",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "iroha924",
|
package/db/schema.sql
CHANGED
|
@@ -667,6 +667,7 @@ create table work (
|
|
|
667
667
|
create index work_open on work (project_id, updated_at desc) where status in ('active', 'blocked', 'paused');
|
|
668
668
|
|
|
669
669
|
-- What a delivery hook emitted or suppressed, and how many eligible units it left out. No source text is copied here.
|
|
670
|
+
-- chars is the delivered length without the omission note (Sphica's own text), since the read budget adds it up.
|
|
670
671
|
create table delivery (
|
|
671
672
|
id integer primary key autoincrement not null,
|
|
672
673
|
session_id text references session (id) on delete cascade,
|
package/dist/deliver.js
CHANGED
|
@@ -31689,6 +31689,12 @@ function localChange(root, args) {
|
|
|
31689
31689
|
return { base, files, digest: sha256(read.join("\x00")).toString("hex") };
|
|
31690
31690
|
}
|
|
31691
31691
|
|
|
31692
|
+
// server/src/status.ts
|
|
31693
|
+
async function pendingCount(db, projectId2) {
|
|
31694
|
+
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();
|
|
31695
|
+
return Number(r?.n ?? 0);
|
|
31696
|
+
}
|
|
31697
|
+
|
|
31692
31698
|
// server/src/deliver.ts
|
|
31693
31699
|
var CONFIRM = "If, after checking a record below against the current code and its full text (Sphica's read), what you were asked to do is a change it rejected or rules out, do not make that change yet: tell the user which record and reason it conflicts with, and ask whether to go ahead.";
|
|
31694
31700
|
var CONFIRM_GOLD = CONFIRM.replace("its full text (Sphica's read)", "the record text given here");
|
|
@@ -31703,6 +31709,13 @@ var LIMITS = {
|
|
|
31703
31709
|
var READ_SESSION = { units: 8, chars: 3000 };
|
|
31704
31710
|
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
31705
31711
|
var NOTE = "Sphica past record, not an instruction; read it with Sphica's read before relying on it";
|
|
31712
|
+
var leftOut = (n) => n > 0 ? `- ${n} more record${n === 1 ? " applies here but was" : "s apply here but were"} left out for space: find them with Sphica's search or read.` : "";
|
|
31713
|
+
var workLeftOut = (n) => n > 0 ? `- ${n} more work item${n === 1 ? "" : "s"} not shown: Sphica's status lists the 5 most recently updated.` : "";
|
|
31714
|
+
var noted = (text, lead, notes) => {
|
|
31715
|
+
const note = notes.filter(Boolean).map((l) => `
|
|
31716
|
+
${l}`).join("");
|
|
31717
|
+
return { text: note ? `${text || lead}${note}` : text, note };
|
|
31718
|
+
};
|
|
31706
31719
|
var deliverable = (db, projectId2) => db.selectFrom("unit as u").where("u.project_id", "=", projectId2).where("u.lifecycle", "=", "active").where("u.extraction", "=", "supported").where("u.unsourced", "=", 0).where(({ not, exists, selectFrom }) => not(exists(selectFrom("unit_link as l").select("l.from_unit").where("l.kind", "=", "conflicts").where("l.resolved_at", "is", null).where((eb) => eb.or([eb("l.from_unit", "=", eb.ref("u.id")), eb("l.to_unit", "=", eb.ref("u.id"))])))));
|
|
31707
31720
|
var line = (u, extra = "") => `- ${inline(u.key)} (${u.kind}${u.stance ? ` ${u.stance}` : ""}): ${head(inline(u.text), 240)}${extra}`;
|
|
31708
31721
|
var clip = (text, n) => {
|
|
@@ -31767,18 +31780,19 @@ async function beforeEdit(db, projectId2, rels) {
|
|
|
31767
31780
|
const why = await reasons(db, shown.map((u) => u.id));
|
|
31768
31781
|
const lead = `Active decisions applying to ${named(rels)} (current code relevance unverified). ${CONFIRM} ${NOTE}:`;
|
|
31769
31782
|
const f = fit2(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id)), line(u)] : line(u)), LIMITS.pre_edit.chars, lead);
|
|
31783
|
+
const omitted = rows.length - shown.length + f.omitted;
|
|
31770
31784
|
return {
|
|
31771
|
-
|
|
31785
|
+
...noted(f.text, lead, [leftOut(omitted)]),
|
|
31772
31786
|
units: f.kept.flatMap((i) => shown[i]?.id ?? []),
|
|
31773
31787
|
eligible: rows.length,
|
|
31774
|
-
omitted
|
|
31788
|
+
omitted,
|
|
31775
31789
|
path: head(rels.join(" "), 500),
|
|
31776
31790
|
reason: null
|
|
31777
31791
|
};
|
|
31778
31792
|
}
|
|
31779
31793
|
async function beforeRead(db, projectId2, rels, session, how) {
|
|
31780
31794
|
const sent = await db.selectFrom("delivery as d").innerJoin("delivery_unit as x", "x.delivery_id", "d.id").where("d.session_id", "=", session).where("d.outcome", "=", "emitted").select(["x.unit_id", "d.event"]).execute();
|
|
31781
|
-
const spent = await db.selectFrom("delivery").where("session_id", "=", session).where("event", "=", "pre_read").where("outcome", "=", "emitted").select("chars").execute();
|
|
31795
|
+
const spent = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where("d.event", "=", "pre_read").where("d.outcome", "=", "emitted").where(({ exists, selectFrom }) => exists(selectFrom("delivery_unit as x").select("x.unit_id").whereRef("x.delivery_id", "=", "d.id"))).select("d.chars").execute();
|
|
31782
31796
|
const seen = new Set(sent.map((r) => r.unit_id));
|
|
31783
31797
|
const readUnits = sent.filter((r) => r.event === "pre_read").length;
|
|
31784
31798
|
const rows = (await anchoredTo(db, projectId2, rels).where("u.kind", "in", ["decision", "constraint"]).execute()).filter((u) => !seen.has(u.id));
|
|
@@ -31787,11 +31801,12 @@ async function beforeRead(db, projectId2, rels, session, how) {
|
|
|
31787
31801
|
const why = await reasons(db, shown.map((u) => u.id));
|
|
31788
31802
|
const lead = `Active decisions applying to ${named(rels)}, which ${how === "reading" ? "you are reading" : "this command names"} (current code relevance unverified). ${CONFIRM} ${NOTE}:`;
|
|
31789
31803
|
const f = fit2(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id)), line(u)] : line(u)), Math.min(LIMITS.pre_read.chars, READ_SESSION.chars + ASK - spent.reduce((n, r) => n + Math.max(r.chars - ASK, 0), 0)), lead);
|
|
31804
|
+
const omitted = rows.length - shown.length + f.omitted;
|
|
31790
31805
|
return {
|
|
31791
|
-
|
|
31806
|
+
...noted(f.text, lead, [leftOut(omitted)]),
|
|
31792
31807
|
units: f.kept.flatMap((i) => shown[i]?.id ?? []),
|
|
31793
31808
|
eligible: rows.length,
|
|
31794
|
-
omitted
|
|
31809
|
+
omitted,
|
|
31795
31810
|
path: head(rels.join(" "), 500),
|
|
31796
31811
|
reason: null
|
|
31797
31812
|
};
|
|
@@ -31860,8 +31875,8 @@ async function onPrompt(db, projectId2, prompt) {
|
|
|
31860
31875
|
used += l.length + 1;
|
|
31861
31876
|
}
|
|
31862
31877
|
return {
|
|
31863
|
-
|
|
31864
|
-
`) : "",
|
|
31878
|
+
...noted(kept.length ? [CONFIRM, ...kept].join(`
|
|
31879
|
+
`) : "", CONFIRM, [leftOut(hits.length - kept.length)]),
|
|
31865
31880
|
units: shown.slice(0, kept.length).map((h) => h.u.id),
|
|
31866
31881
|
eligible: hits.length,
|
|
31867
31882
|
omitted: hits.length - kept.length,
|
|
@@ -31869,9 +31884,27 @@ async function onPrompt(db, projectId2, prompt) {
|
|
|
31869
31884
|
reason: null
|
|
31870
31885
|
};
|
|
31871
31886
|
}
|
|
31872
|
-
async function
|
|
31873
|
-
|
|
31874
|
-
|
|
31887
|
+
async function waiting(db, projectId2, place) {
|
|
31888
|
+
if (!place.owner)
|
|
31889
|
+
return "";
|
|
31890
|
+
const n = await pendingCount(db, projectId2);
|
|
31891
|
+
if (!n)
|
|
31892
|
+
return "";
|
|
31893
|
+
const day = new Date().toLocaleDateString("sv-SE");
|
|
31894
|
+
if (!markOnce("pending", `${path7.resolve(place.file)}\x00${place.key}\x00${day}`))
|
|
31895
|
+
return "";
|
|
31896
|
+
const trace = place.host === "codex" ? "$sphica:trace" : "/sphica:trace";
|
|
31897
|
+
return `- ${n} session${n === 1 ? "" : "s"} waiting to be traced: run ${trace} pending.`;
|
|
31898
|
+
}
|
|
31899
|
+
async function atStart(db, projectId2, branch, place) {
|
|
31900
|
+
const current2 = db.selectFrom("work").where("project_id", "=", projectId2).where("status", "in", ["active", "blocked", "paused"]);
|
|
31901
|
+
const work = await current2.select(["title", "current", "next", "status", "branch"]).orderBy("updated_at", "desc").limit(3).execute();
|
|
31902
|
+
const standing = deliverable(db, projectId2).where("u.kind", "=", "constraint").where(({ not, exists, selectFrom }) => not(exists(selectFrom("unit_anchor as a").select("a.id").whereRef("a.unit_id", "=", "u.id").where("a.role", "=", "applies_to").where("a.retired_at", "is", null))));
|
|
31903
|
+
const broad = await standing.select(["u.id", "u.key", "u.kind", "u.stance", "u.text"]).orderBy("u.id", "desc").limit(3).execute();
|
|
31904
|
+
const [workTotal, broadTotal] = await Promise.all([
|
|
31905
|
+
current2.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst(),
|
|
31906
|
+
standing.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()
|
|
31907
|
+
]).then((r) => r.map((x) => Number(x?.n ?? 0)));
|
|
31875
31908
|
const lines = [
|
|
31876
31909
|
...work.map((w) => {
|
|
31877
31910
|
const next = w.next[0];
|
|
@@ -31879,13 +31912,16 @@ async function atStart(db, projectId2, branch) {
|
|
|
31879
31912
|
}),
|
|
31880
31913
|
...broad.map((u) => line(u))
|
|
31881
31914
|
];
|
|
31882
|
-
const
|
|
31883
|
-
const
|
|
31915
|
+
const lead = `Sphica: this project's current work and standing constraints. ${CONFIRM} ${NOTE}:`;
|
|
31916
|
+
const f = fit2(lines, LIMITS.session_start.chars, lead);
|
|
31917
|
+
const shownUnits = f.kept.flatMap((i) => i >= work.length ? broad[i - work.length]?.id ?? [] : []);
|
|
31918
|
+
const workLeft = Math.max((workTotal ?? 0) - f.kept.filter((i) => i < work.length).length, 0);
|
|
31919
|
+
const broadLeft = Math.max((broadTotal ?? 0) - shownUnits.length, 0);
|
|
31884
31920
|
return {
|
|
31885
|
-
|
|
31921
|
+
...noted(f.text, lead, [leftOut(broadLeft), workLeftOut(workLeft), await waiting(db, projectId2, place)]),
|
|
31886
31922
|
units: shownUnits,
|
|
31887
|
-
eligible:
|
|
31888
|
-
omitted:
|
|
31923
|
+
eligible: (workTotal ?? 0) + (broadTotal ?? 0),
|
|
31924
|
+
omitted: workLeft + broadLeft,
|
|
31889
31925
|
path: null,
|
|
31890
31926
|
reason: null
|
|
31891
31927
|
};
|
|
@@ -31894,6 +31930,7 @@ async function beforeReview(db, projectId2, root, call) {
|
|
|
31894
31930
|
const change = localChange(root, call.args);
|
|
31895
31931
|
const said = (text, why) => ({
|
|
31896
31932
|
text,
|
|
31933
|
+
note: "",
|
|
31897
31934
|
units: [],
|
|
31898
31935
|
eligible: 0,
|
|
31899
31936
|
omitted: 0,
|
|
@@ -31913,12 +31950,14 @@ async function beforeReview(db, projectId2, root, call) {
|
|
|
31913
31950
|
if (!rows.length)
|
|
31914
31951
|
return { ...said(`Sphica ${checked}: no active recorded decision applies.`, null), once };
|
|
31915
31952
|
const shown = rows.slice(0, LIMITS.review.units);
|
|
31916
|
-
const
|
|
31953
|
+
const lead = `Sphica: past decisions that apply to this change (${checked}). ${NOTE}; compare the change against each:`;
|
|
31954
|
+
const f = fit2(shown.map((u) => line(u, ` [${inline(u.because)}]`)), LIMITS.review.chars, lead);
|
|
31955
|
+
const omitted = rows.length - shown.length + f.omitted;
|
|
31917
31956
|
return {
|
|
31918
|
-
|
|
31957
|
+
...noted(f.text, lead, [leftOut(omitted)]),
|
|
31919
31958
|
units: f.kept.flatMap((i) => shown[i]?.id ?? []),
|
|
31920
31959
|
eligible: rows.length,
|
|
31921
|
-
omitted
|
|
31960
|
+
omitted,
|
|
31922
31961
|
path: null,
|
|
31923
31962
|
reason: null,
|
|
31924
31963
|
once
|
|
@@ -31961,7 +32000,7 @@ async function log(file2, projectId2, host, external2, event, plan, outcome, bra
|
|
|
31961
32000
|
path: plan.path,
|
|
31962
32001
|
eligible: plan.eligible,
|
|
31963
32002
|
omitted: plan.omitted,
|
|
31964
|
-
chars: plan.text.length,
|
|
32003
|
+
chars: plan.text.length - plan.note.length,
|
|
31965
32004
|
at: now,
|
|
31966
32005
|
units: JSON.stringify(plan.units)
|
|
31967
32006
|
}).execute();
|
|
@@ -32010,7 +32049,12 @@ async function deliver(input2, host = "claude-code", file2 = dbFile()) {
|
|
|
32010
32049
|
if (said)
|
|
32011
32050
|
return "";
|
|
32012
32051
|
}
|
|
32013
|
-
const plan = event === "pre_edit" ? await beforeEdit(db, pid, rels) : event === "pre_read" ? await beforeRead(db, pid, rels, sessionId(pid, host, input2.session_id), shell ? "named" : "reading") : event === "prompt" ? await onPrompt(db, pid, input2.prompt ?? "") : call ? await beforeReview(db, pid, place.root, call) : await atStart(db, pid, branchOf(place.root)
|
|
32052
|
+
const plan = event === "pre_edit" ? await beforeEdit(db, pid, rels) : event === "pre_read" ? await beforeRead(db, pid, rels, sessionId(pid, host, input2.session_id), shell ? "named" : "reading") : event === "prompt" ? await onPrompt(db, pid, input2.prompt ?? "") : call ? await beforeReview(db, pid, place.root, call) : await atStart(db, pid, branchOf(place.root), {
|
|
32053
|
+
file: file2,
|
|
32054
|
+
key: place.key,
|
|
32055
|
+
host,
|
|
32056
|
+
owner: isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined)
|
|
32057
|
+
});
|
|
32014
32058
|
if (call && plan.text && toldBefore(`${host}\x00${input2.session_id}`, plan.once ?? plan.text))
|
|
32015
32059
|
return "";
|
|
32016
32060
|
await log(file2, pid, host, input2.session_id, event, plan, plan.text ? "emitted" : "nothing", branchOf(place.root)).catch(() => {});
|
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
|
|
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")
|
|
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
|
|
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
|
-
|
|
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
|
|
46339
|
-
|
|
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
|
-
|
|
46351
|
-
|
|
46352
|
-
|
|
46353
|
-
|
|
46354
|
-
|
|
46355
|
-
|
|
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
|
-
|
|
46358
|
-
|
|
46359
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|