sphica 0.6.18 → 0.6.20
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/dist/capture.js +1 -0
- package/dist/cli.js +525 -0
- package/dist/deliver.js +1 -0
- package/dist/mcp-record.js +12 -11
- package/dist/mcp.js +675 -577
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -34743,6 +34743,7 @@ function singular(w) {
|
|
|
34743
34743
|
return w.slice(0, -1);
|
|
34744
34744
|
return w.endsWith("s") && !/(?:ss|us|is)$/.test(w) ? w.slice(0, -1) : w;
|
|
34745
34745
|
}
|
|
34746
|
+
var identTerm = (x) => singular(x.normalize("NFKC").toLowerCase());
|
|
34746
34747
|
function terms(text) {
|
|
34747
34748
|
const norm = text.normalize("NFKC").toLowerCase();
|
|
34748
34749
|
const out = [];
|
|
@@ -34801,8 +34802,9 @@ var QUESTION = new Set([
|
|
|
34801
34802
|
...["add", "get", "make", "have", "has", "not", "no", "yes", "reason"],
|
|
34802
34803
|
...["理由", "仕組", "何", "方", "場合", "今", "件"]
|
|
34803
34804
|
]);
|
|
34805
|
+
var FOLDED_QUESTION = new Set([...QUESTION].flatMap((w) => terms(w)));
|
|
34804
34806
|
function queryTerms(question) {
|
|
34805
|
-
return [...new Set(terms(question).filter((w) => !
|
|
34807
|
+
return [...new Set(terms(question).filter((w) => !FOLDED_QUESTION.has(w)))].slice(0, 24);
|
|
34806
34808
|
}
|
|
34807
34809
|
function ftsQuery(question) {
|
|
34808
34810
|
const ws = queryTerms(question);
|
|
@@ -45627,406 +45629,65 @@ function parseObject(obj, jsonPath, options) {
|
|
|
45627
45629
|
return target;
|
|
45628
45630
|
}
|
|
45629
45631
|
|
|
45630
|
-
// server/src/
|
|
45631
|
-
var
|
|
45632
|
-
|
|
45633
|
-
|
|
45634
|
-
|
|
45635
|
-
|
|
45636
|
-
|
|
45637
|
-
|
|
45638
|
-
async function searchUnits(db, projectId, q) {
|
|
45639
|
-
const wanted = queryTerms(q.question);
|
|
45640
|
-
const match = ftsQuery(q.question);
|
|
45641
|
-
if (!match)
|
|
45642
|
-
return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0 };
|
|
45643
|
-
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 = ${projectId})`.as("f")).innerJoin("unit as u", "u.id", "f.rowid").where("u.project_id", "=", projectId).where("u.extraction", "=", "supported");
|
|
45644
|
-
if (q.kinds?.length)
|
|
45645
|
-
query = query.where("u.kind", "in", q.kinds);
|
|
45646
|
-
if (q.lifecycles?.length)
|
|
45647
|
-
query = query.where("u.lifecycle", "in", q.lifecycles);
|
|
45648
|
-
if (q.path)
|
|
45649
|
-
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)));
|
|
45650
|
-
const ranked = await query.select(["u.id", "f.rank"]).orderBy("f.rank").orderBy("u.id").limit(UNIT_SCAN_MAX + 1).execute();
|
|
45651
|
-
const stopped = ranked.length > UNIT_SCAN_MAX;
|
|
45652
|
-
let weaker = 0;
|
|
45653
|
-
let read = 0;
|
|
45654
|
-
const hits = [];
|
|
45655
|
-
for (let at = 0;at < Math.min(ranked.length, UNIT_SCAN_MAX); at += UNIT_PAGE) {
|
|
45656
|
-
const part = ranked.slice(at, Math.min(at + UNIT_PAGE, UNIT_SCAN_MAX));
|
|
45657
|
-
const order = new Map(part.map((r, i) => [r.id, i]));
|
|
45658
|
-
const rows = (await query.select([
|
|
45659
|
-
"u.id",
|
|
45660
|
-
"u.key",
|
|
45661
|
-
"u.kind",
|
|
45662
|
-
"u.stance",
|
|
45663
|
-
"u.lifecycle",
|
|
45664
|
-
"u.text",
|
|
45665
|
-
"u.why",
|
|
45666
|
-
"u.scope_note",
|
|
45667
|
-
"u.revisit_when",
|
|
45668
|
-
"u.content_hash",
|
|
45669
|
-
"f.rank"
|
|
45670
|
-
]).where("u.id", "in", part.map((r) => r.id)).execute()).sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
|
|
45671
|
-
read += rows.length;
|
|
45672
|
-
weaker += await judgeUnits(db, rows, wanted, hits);
|
|
45673
|
-
}
|
|
45674
|
-
const shown = new Set(hits.map((h) => h.id));
|
|
45675
|
-
for (const h of [...hits].filter((x) => x.lifecycle === "superseded"))
|
|
45676
|
-
for (const n of await liveSuccessors(db, h.id))
|
|
45677
|
-
if (!shown.has(n.id) && (!q.kinds?.length || q.kinds.includes(n.kind)) && (!q.lifecycles?.length || q.lifecycles.includes(n.lifecycle))) {
|
|
45678
|
-
shown.add(n.id);
|
|
45679
|
-
hits.push({
|
|
45680
|
-
...n,
|
|
45681
|
-
options: [],
|
|
45682
|
-
anchors: [],
|
|
45683
|
-
matched: [],
|
|
45684
|
-
aliasOnly: false,
|
|
45685
|
-
successorOf: h.key,
|
|
45686
|
-
rank: h.rank
|
|
45687
|
-
});
|
|
45688
|
-
}
|
|
45689
|
-
hits.sort((a, b) => (LIFE_ORDER[a.lifecycle] ?? 9) - (LIFE_ORDER[b.lifecycle] ?? 9) || b.matched.length - a.matched.length || a.rank - b.rank);
|
|
45632
|
+
// server/src/kysely-node-sqlite.ts
|
|
45633
|
+
var plain2 = (row) => {
|
|
45634
|
+
const out = {};
|
|
45635
|
+
for (const [k, v] of Object.entries(row))
|
|
45636
|
+
out[k] = v instanceof Uint8Array && !Buffer.isBuffer(v) ? Buffer.from(v.buffer, v.byteOffset, v.byteLength) : v;
|
|
45637
|
+
return out;
|
|
45638
|
+
};
|
|
45639
|
+
function adapt(raw) {
|
|
45690
45640
|
return {
|
|
45691
|
-
|
|
45692
|
-
|
|
45693
|
-
|
|
45694
|
-
|
|
45695
|
-
|
|
45641
|
+
prepare(sqlText) {
|
|
45642
|
+
const st = raw.prepare(sqlText);
|
|
45643
|
+
const args = (p) => p;
|
|
45644
|
+
return {
|
|
45645
|
+
get reader() {
|
|
45646
|
+
return st.columns().length > 0;
|
|
45647
|
+
},
|
|
45648
|
+
all: (p) => st.all(...args(p)).map(plain2),
|
|
45649
|
+
run: (p) => {
|
|
45650
|
+
const r = st.run(...args(p));
|
|
45651
|
+
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
45652
|
+
},
|
|
45653
|
+
iterate: (p) => st.iterate(...args(p))
|
|
45654
|
+
};
|
|
45655
|
+
},
|
|
45656
|
+
close: () => raw.close()
|
|
45696
45657
|
};
|
|
45697
45658
|
}
|
|
45698
|
-
|
|
45699
|
-
|
|
45700
|
-
|
|
45701
|
-
|
|
45702
|
-
|
|
45703
|
-
|
|
45704
|
-
|
|
45705
|
-
|
|
45706
|
-
|
|
45707
|
-
|
|
45708
|
-
|
|
45709
|
-
|
|
45710
|
-
|
|
45711
|
-
|
|
45712
|
-
|
|
45713
|
-
|
|
45714
|
-
|
|
45715
|
-
|
|
45716
|
-
|
|
45717
|
-
if (seen.has(n.id))
|
|
45718
|
-
continue;
|
|
45719
|
-
seen.add(n.id);
|
|
45720
|
-
found.push(n);
|
|
45721
|
-
frontier.push(n.id);
|
|
45722
|
-
}
|
|
45659
|
+
|
|
45660
|
+
// server/src/sqlite.ts
|
|
45661
|
+
import fs2 from "node:fs";
|
|
45662
|
+
import os from "node:os";
|
|
45663
|
+
import path2 from "node:path";
|
|
45664
|
+
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
45665
|
+
|
|
45666
|
+
// server/src/plugin.ts
|
|
45667
|
+
import fs from "node:fs";
|
|
45668
|
+
import path from "node:path";
|
|
45669
|
+
import { fileURLToPath } from "node:url";
|
|
45670
|
+
var MANIFEST = path.join(".claude-plugin", "plugin.json");
|
|
45671
|
+
var PACKAGE = "package.json";
|
|
45672
|
+
function versionAt(root) {
|
|
45673
|
+
try {
|
|
45674
|
+
const m = JSON.parse(fs.readFileSync(path.join(root, MANIFEST), "utf8"));
|
|
45675
|
+
return m.name === "sphica" && typeof m.version === "string" ? m.version : null;
|
|
45676
|
+
} catch {
|
|
45677
|
+
return null;
|
|
45723
45678
|
}
|
|
45724
|
-
return found.filter((n) => !replaced.has(n.id));
|
|
45725
45679
|
}
|
|
45726
|
-
|
|
45727
|
-
|
|
45728
|
-
|
|
45729
|
-
|
|
45730
|
-
|
|
45731
|
-
|
|
45732
|
-
db.selectFrom("unit_field as f").innerJoin("field_def as d", "d.id", "f.field_def_id").select(["f.unit_id", "d.name", "f.value"]).where("f.unit_id", "in", ids).execute()
|
|
45733
|
-
]) : [[], [], [], []];
|
|
45734
|
-
let weaker = 0;
|
|
45735
|
-
for (const r of rows) {
|
|
45736
|
-
const opts = options.filter((o) => o.unit_id === r.id);
|
|
45737
|
-
const anch = anchors.filter((a) => a.unit_id === r.id);
|
|
45738
|
-
const alias = aliases.find((a) => a.unit_id === r.id && Buffer.from(a.content_hash).equals(Buffer.from(r.content_hash)));
|
|
45739
|
-
const own2 = new Set(terms([
|
|
45740
|
-
r.text,
|
|
45741
|
-
r.why,
|
|
45742
|
-
r.scope_note,
|
|
45743
|
-
r.revisit_when,
|
|
45744
|
-
...opts.flatMap((o) => [o.text, o.why, o.reconsider_when]),
|
|
45745
|
-
...anch.flatMap((a) => [a.path, a.symbol]),
|
|
45746
|
-
...fields.filter((f) => f.unit_id === r.id).flatMap((f) => [f.name, f.value])
|
|
45747
|
-
].filter(Boolean).join(`
|
|
45748
|
-
`)));
|
|
45749
|
-
const extra = new Set(alias ? terms(JSON.parse(alias.terms).join(" ")) : []);
|
|
45750
|
-
const matched = wanted.filter((w) => own2.has(w) || extra.has(w));
|
|
45751
|
-
const ident = new Set(anch.flatMap((a) => [a.path, a.symbol].flatMap((x) => x ? [x.normalize("NFKC").toLowerCase()] : [])));
|
|
45752
|
-
if (!strong(matched.length, wanted.length) && !matched.some((w) => ident.has(w))) {
|
|
45753
|
-
weaker++;
|
|
45754
|
-
continue;
|
|
45755
|
-
}
|
|
45756
|
-
hits.push({
|
|
45757
|
-
id: r.id,
|
|
45758
|
-
key: r.key,
|
|
45759
|
-
kind: r.kind,
|
|
45760
|
-
stance: r.stance,
|
|
45761
|
-
lifecycle: r.lifecycle,
|
|
45762
|
-
text: r.text,
|
|
45763
|
-
why: r.why,
|
|
45764
|
-
revisit_when: r.revisit_when,
|
|
45765
|
-
options: opts.map((o) => ({ text: o.text, outcome: o.outcome, why: o.why })),
|
|
45766
|
-
anchors: anch.map((a) => ({ path: a.path, symbol: a.symbol, role: a.role })),
|
|
45767
|
-
matched,
|
|
45768
|
-
aliasOnly: matched.every((w) => !own2.has(w)),
|
|
45769
|
-
rank: r.rank
|
|
45770
|
-
});
|
|
45680
|
+
function packageVersionAt(root) {
|
|
45681
|
+
try {
|
|
45682
|
+
const m = JSON.parse(fs.readFileSync(path.join(root, PACKAGE), "utf8"));
|
|
45683
|
+
return m.name === "sphica" && typeof m.version === "string" ? m.version : null;
|
|
45684
|
+
} catch {
|
|
45685
|
+
return null;
|
|
45771
45686
|
}
|
|
45772
|
-
return weaker;
|
|
45773
|
-
}
|
|
45774
|
-
async function searchSources(db, projectId, question, limit, owner) {
|
|
45775
|
-
const wanted = queryTerms(question);
|
|
45776
|
-
const match = ftsQuery(question);
|
|
45777
|
-
if (!match)
|
|
45778
|
-
return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0 };
|
|
45779
|
-
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 = ${projectId})`.as("f")).innerJoin("source as s", "s.id", "f.rowid").where("s.project_id", "=", projectId).$if(owner !== undefined, (q) => q.where("s.kind", "=", "session_message").where("s.author_kind", "=", "owner").$if((owner?.notSessions.length ?? 0) > 0, (q2) => q2.where("s.session_id", "not in", owner?.notSessions ?? [])));
|
|
45780
|
-
const ranked = await query.select(["s.id", "f.rank"]).orderBy("f.rank").orderBy("s.id").limit(SOURCE_SCAN_MAX + 1).execute();
|
|
45781
|
-
let weaker = 0;
|
|
45782
|
-
let stopped = false;
|
|
45783
|
-
const hits = [];
|
|
45784
|
-
let read = 0;
|
|
45785
|
-
let bytes2 = 0;
|
|
45786
|
-
scan:
|
|
45787
|
-
for (let at = 0;at < ranked.length; at += SOURCE_PAGE) {
|
|
45788
|
-
const ids = ranked.slice(at, at + SOURCE_PAGE).map((r) => r.id);
|
|
45789
|
-
const byId = new Map((await db.selectFrom("source").select([
|
|
45790
|
-
"id",
|
|
45791
|
-
"kind",
|
|
45792
|
-
"artifact",
|
|
45793
|
-
"author_kind",
|
|
45794
|
-
"author_login",
|
|
45795
|
-
"created_at",
|
|
45796
|
-
"text",
|
|
45797
|
-
"session_id",
|
|
45798
|
-
"turn_id"
|
|
45799
|
-
]).where("id", "in", ids).execute()).map((r) => [r.id, r]));
|
|
45800
|
-
for (const id of ids) {
|
|
45801
|
-
if (read >= SOURCE_SCAN_MAX || bytes2 >= SOURCE_SCAN_BYTES) {
|
|
45802
|
-
stopped = true;
|
|
45803
|
-
break scan;
|
|
45804
|
-
}
|
|
45805
|
-
const r = byId.get(id);
|
|
45806
|
-
if (!r)
|
|
45807
|
-
continue;
|
|
45808
|
-
read++;
|
|
45809
|
-
bytes2 += Buffer.byteLength(r.text);
|
|
45810
|
-
const own2 = new Set(terms(r.text));
|
|
45811
|
-
const matched = wanted.filter((w) => own2.has(w));
|
|
45812
|
-
if (!strong(matched.length, wanted.length)) {
|
|
45813
|
-
weaker++;
|
|
45814
|
-
continue;
|
|
45815
|
-
}
|
|
45816
|
-
hits.push({
|
|
45817
|
-
id: r.id,
|
|
45818
|
-
kind: r.kind,
|
|
45819
|
-
artifact: r.artifact,
|
|
45820
|
-
author: r.author_login ?? r.author_kind,
|
|
45821
|
-
created_at: r.created_at,
|
|
45822
|
-
text: r.text,
|
|
45823
|
-
matched,
|
|
45824
|
-
session: r.session_id,
|
|
45825
|
-
turn: r.turn_id
|
|
45826
|
-
});
|
|
45827
|
-
if (hits.length >= limit)
|
|
45828
|
-
break scan;
|
|
45829
|
-
}
|
|
45830
|
-
}
|
|
45831
|
-
if (!stopped && hits.length < limit && ranked.length > SOURCE_SCAN_MAX)
|
|
45832
|
-
stopped = true;
|
|
45833
|
-
return { hits, weaker, terms: wanted, stopped, read };
|
|
45834
45687
|
}
|
|
45835
|
-
|
|
45836
|
-
|
|
45837
|
-
var
|
|
45838
|
-
var DECIDED_LIFECYCLES = new Set(["active", "superseded", "withdrawn"]);
|
|
45839
|
-
var PER_MESSAGE = 10;
|
|
45840
|
-
async function askedBefore(db, projectId, q) {
|
|
45841
|
-
const kept = (u) => (!q.kinds?.length || q.kinds.includes(u.kind)) && (!q.lifecycles?.length || q.lifecycles.includes(u.lifecycle));
|
|
45842
|
-
const found = await searchSources(db, projectId, q.question, Number.POSITIVE_INFINITY, {
|
|
45843
|
-
notSessions: q.notSessions
|
|
45844
|
-
});
|
|
45845
|
-
const ids = found.hits.map((h) => h.id);
|
|
45846
|
-
const cited = ids.length ? await citing(db, ids) : [];
|
|
45847
|
-
const traced = new Set(ids.length ? (await db.selectFrom("source_processing").select("source_id").where("source_id", "in", ids).execute()).map((r) => r.source_id) : []);
|
|
45848
|
-
const decided = new Set(cited.filter((c) => DECIDED_KINDS.has(c.kind) && DECIDED_LIFECYCLES.has(c.lifecycle)).map((c) => c.source_id));
|
|
45849
|
-
const all = [];
|
|
45850
|
-
for (const m of found.hits) {
|
|
45851
|
-
const direct = unique(cited.filter((c) => c.source_id === m.id));
|
|
45852
|
-
const shown = direct.filter(kept);
|
|
45853
|
-
all.push({
|
|
45854
|
-
message: m,
|
|
45855
|
-
led: shown.map((u) => ({ ...u, now: [] })),
|
|
45856
|
-
hidden: direct.length - shown.length,
|
|
45857
|
-
context: [],
|
|
45858
|
-
direct: new Set(direct.map((u) => u.id)),
|
|
45859
|
-
decided: decided.has(m.id),
|
|
45860
|
-
traced: traced.has(m.id),
|
|
45861
|
-
reply: null
|
|
45862
|
-
});
|
|
45863
|
-
}
|
|
45864
|
-
const messages = all.slice(0, q.limit);
|
|
45865
|
-
for (const e of messages) {
|
|
45866
|
-
for (const u of e.led)
|
|
45867
|
-
if (u.lifecycle === "superseded")
|
|
45868
|
-
u.now = await liveSuccessors(db, u.id);
|
|
45869
|
-
await sameTurn(db, e, kept);
|
|
45870
|
-
}
|
|
45871
|
-
const sessions = new Map;
|
|
45872
|
-
for (const e of all) {
|
|
45873
|
-
const s = e.message.session ?? "";
|
|
45874
|
-
const was = sessions.get(s) ?? { decided: false, traced: true };
|
|
45875
|
-
sessions.set(s, { decided: was.decided || e.decided, traced: was.traced && e.traced });
|
|
45876
|
-
}
|
|
45877
|
-
const none = [...sessions.values()].every((s) => !s.decided);
|
|
45878
|
-
const repeats = sessions.size >= 2 && none ? {
|
|
45879
|
-
untraced: [...sessions].filter(([, s]) => !s.traced).map(([id]) => id),
|
|
45880
|
-
traced: [...sessions].filter(([, s]) => s.traced).map(([id]) => id)
|
|
45881
|
-
} : null;
|
|
45882
|
-
return {
|
|
45883
|
-
messages,
|
|
45884
|
-
repeats,
|
|
45885
|
-
terms: found.terms,
|
|
45886
|
-
weaker: found.weaker,
|
|
45887
|
-
stopped: found.stopped,
|
|
45888
|
-
read: found.read
|
|
45889
|
-
};
|
|
45890
|
-
}
|
|
45891
|
-
async function citing(db, sources) {
|
|
45892
|
-
const [evidence, adoption] = await Promise.all([
|
|
45893
|
-
db.selectFrom("unit_evidence as e").innerJoin("unit as u", "u.id", "e.unit_id").where("e.source_id", "in", sources).where("e.retracted_at", "is", null).where("u.extraction", "=", "supported").select(["e.source_id", "e.role", "u.id", "u.key", "u.kind", "u.lifecycle"]).orderBy("u.id").execute(),
|
|
45894
|
-
db.selectFrom("unit_adoption as a").innerJoin("unit as u", "u.id", "a.unit_id").where("a.source_id", "in", sources).where("a.retracted_at", "is", null).where("u.extraction", "=", "supported").select(["a.source_id", "u.id", "u.key", "u.kind", "u.lifecycle"]).orderBy("u.id").execute()
|
|
45895
|
-
]);
|
|
45896
|
-
return [...evidence, ...adoption.map((a) => ({ ...a, role: "adoption" }))];
|
|
45897
|
-
}
|
|
45898
|
-
var unique = (xs) => [...new Map(xs.map((x) => [x.id, x])).values()];
|
|
45899
|
-
async function sameTurn(db, e, kept) {
|
|
45900
|
-
const { session, turn, id } = e.message;
|
|
45901
|
-
if (!session || !turn)
|
|
45902
|
-
return;
|
|
45903
|
-
const others = await db.selectFrom("source").select(["id", "author_kind"]).where("session_id", "=", session).where("turn_id", "=", turn).where("id", "<>", id).orderBy("id").execute();
|
|
45904
|
-
e.reply = others.findLast((o) => o.author_kind === "assistant")?.id ?? null;
|
|
45905
|
-
if (!others.length)
|
|
45906
|
-
return;
|
|
45907
|
-
const speaker = new Map(others.map((o) => [o.id, o.author_kind]));
|
|
45908
|
-
const seen = new Set;
|
|
45909
|
-
for (const c of await citing(db, others.map((o) => o.id))) {
|
|
45910
|
-
const k = `${c.id}\x00${c.source_id}\x00${c.role}`;
|
|
45911
|
-
if (e.direct.has(c.id) || !kept(c) || seen.has(k))
|
|
45912
|
-
continue;
|
|
45913
|
-
seen.add(k);
|
|
45914
|
-
e.context.push({
|
|
45915
|
-
key: c.key,
|
|
45916
|
-
kind: c.kind,
|
|
45917
|
-
lifecycle: c.lifecycle,
|
|
45918
|
-
speaker: speaker.get(c.source_id) ?? "",
|
|
45919
|
-
role: c.role
|
|
45920
|
-
});
|
|
45921
|
-
}
|
|
45922
|
-
}
|
|
45923
|
-
var UNKNOWN_SESSION = "Current session unknown; results and session counts may include its messages. Pass session to exclude it.";
|
|
45924
|
-
function askedText(r, known = true) {
|
|
45925
|
-
const parts = r.messages.map((e) => {
|
|
45926
|
-
const m = e.message;
|
|
45927
|
-
const lines = [`## s${m.id}: ${inline(m.artifact)}, ${m.created_at}`, inline(head(m.text, 600))];
|
|
45928
|
-
if (e.led.length) {
|
|
45929
|
-
lines.push("Led to:");
|
|
45930
|
-
for (const u of e.led.slice(0, PER_MESSAGE))
|
|
45931
|
-
lines.push(`- ${inline(u.key)} (${u.kind}, ${u.lifecycle}${u.now.length ? `; now ${u.now.map((n) => `${inline(n.key)} (${n.lifecycle})`).join(", ")}` : ""})`);
|
|
45932
|
-
}
|
|
45933
|
-
if (e.led.length > PER_MESSAGE)
|
|
45934
|
-
lines.push(`and ${e.led.length - PER_MESSAGE} more tied records`);
|
|
45935
|
-
if (e.hidden)
|
|
45936
|
-
lines.push(`${e.hidden} tied record${e.hidden === 1 ? "" : "s"} hidden by the filters.`);
|
|
45937
|
-
if (!e.decided)
|
|
45938
|
-
lines.push(`No recorded decision.${e.traced ? "" : ` Not traced yet: run /sphica:trace ${inline(m.session ?? "")}.`}`);
|
|
45939
|
-
if (e.context.length) {
|
|
45940
|
-
lines.push("Same turn (context, not necessarily the answer):");
|
|
45941
|
-
for (const c of e.context.slice(0, PER_MESSAGE))
|
|
45942
|
-
lines.push(`- ${inline(c.key)} (${c.kind}, ${c.lifecycle}): quotes the ${c.speaker} (${c.role})`);
|
|
45943
|
-
if (e.context.length > PER_MESSAGE)
|
|
45944
|
-
lines.push(`and ${e.context.length - PER_MESSAGE} more in the same turn`);
|
|
45945
|
-
}
|
|
45946
|
-
if (e.reply !== null)
|
|
45947
|
-
lines.push(`Reply: s${e.reply} (read it with read)`);
|
|
45948
|
-
return lines.join(`
|
|
45949
|
-
`);
|
|
45950
|
-
});
|
|
45951
|
-
const out = [`${known ? "Earlier owner" : "Owner"} messages matching: ${r.terms.join(", ")}`, ...parts];
|
|
45952
|
-
if (r.repeats) {
|
|
45953
|
-
const n = r.repeats.untraced.length + r.repeats.traced.length;
|
|
45954
|
-
const within = r.stopped ? ` (within the first ${r.read} candidates by rank)` : "";
|
|
45955
|
-
out.push([
|
|
45956
|
-
known ? `Asked in ${n} sessions with no recorded decision${within}:` : `Matching messages in ${n} sessions with no recorded decision (current session may be included)${within}:`,
|
|
45957
|
-
...r.repeats.untraced.length ? [`- not traced yet: ${r.repeats.untraced.map(inline).join(", ")} (run /sphica:trace with each)`] : [],
|
|
45958
|
-
...r.repeats.traced.length ? [`- traced: ${r.repeats.traced.map(inline).join(", ")}`] : []
|
|
45959
|
-
].join(`
|
|
45960
|
-
`));
|
|
45961
|
-
}
|
|
45962
|
-
if (r.stopped)
|
|
45963
|
-
out.push(`Stopped after ${r.read} candidates by rank; more may match.`);
|
|
45964
|
-
if (!known)
|
|
45965
|
-
out.push(UNKNOWN_SESSION);
|
|
45966
|
-
return out.join(`
|
|
45967
|
-
|
|
45968
|
-
`);
|
|
45969
|
-
}
|
|
45970
|
-
|
|
45971
|
-
// server/src/kysely-node-sqlite.ts
|
|
45972
|
-
var plain2 = (row) => {
|
|
45973
|
-
const out = {};
|
|
45974
|
-
for (const [k, v] of Object.entries(row))
|
|
45975
|
-
out[k] = v instanceof Uint8Array && !Buffer.isBuffer(v) ? Buffer.from(v.buffer, v.byteOffset, v.byteLength) : v;
|
|
45976
|
-
return out;
|
|
45977
|
-
};
|
|
45978
|
-
function adapt(raw) {
|
|
45979
|
-
return {
|
|
45980
|
-
prepare(sqlText) {
|
|
45981
|
-
const st = raw.prepare(sqlText);
|
|
45982
|
-
const args = (p) => p;
|
|
45983
|
-
return {
|
|
45984
|
-
get reader() {
|
|
45985
|
-
return st.columns().length > 0;
|
|
45986
|
-
},
|
|
45987
|
-
all: (p) => st.all(...args(p)).map(plain2),
|
|
45988
|
-
run: (p) => {
|
|
45989
|
-
const r = st.run(...args(p));
|
|
45990
|
-
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
45991
|
-
},
|
|
45992
|
-
iterate: (p) => st.iterate(...args(p))
|
|
45993
|
-
};
|
|
45994
|
-
},
|
|
45995
|
-
close: () => raw.close()
|
|
45996
|
-
};
|
|
45997
|
-
}
|
|
45998
|
-
|
|
45999
|
-
// server/src/sqlite.ts
|
|
46000
|
-
import fs2 from "node:fs";
|
|
46001
|
-
import os from "node:os";
|
|
46002
|
-
import path2 from "node:path";
|
|
46003
|
-
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
46004
|
-
|
|
46005
|
-
// server/src/plugin.ts
|
|
46006
|
-
import fs from "node:fs";
|
|
46007
|
-
import path from "node:path";
|
|
46008
|
-
import { fileURLToPath } from "node:url";
|
|
46009
|
-
var MANIFEST = path.join(".claude-plugin", "plugin.json");
|
|
46010
|
-
var PACKAGE = "package.json";
|
|
46011
|
-
function versionAt(root) {
|
|
46012
|
-
try {
|
|
46013
|
-
const m = JSON.parse(fs.readFileSync(path.join(root, MANIFEST), "utf8"));
|
|
46014
|
-
return m.name === "sphica" && typeof m.version === "string" ? m.version : null;
|
|
46015
|
-
} catch {
|
|
46016
|
-
return null;
|
|
46017
|
-
}
|
|
46018
|
-
}
|
|
46019
|
-
function packageVersionAt(root) {
|
|
46020
|
-
try {
|
|
46021
|
-
const m = JSON.parse(fs.readFileSync(path.join(root, PACKAGE), "utf8"));
|
|
46022
|
-
return m.name === "sphica" && typeof m.version === "string" ? m.version : null;
|
|
46023
|
-
} catch {
|
|
46024
|
-
return null;
|
|
46025
|
-
}
|
|
46026
|
-
}
|
|
46027
|
-
var here = path.dirname(fileURLToPath(import.meta.url));
|
|
46028
|
-
var ROOT = [path.join(here, ".."), path.join(here, "..", "..", "plugin")].find((r) => versionAt(r) !== null) ?? path.join(here, "..");
|
|
46029
|
-
var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
45688
|
+
var here = path.dirname(fileURLToPath(import.meta.url));
|
|
45689
|
+
var ROOT = [path.join(here, ".."), path.join(here, "..", "..", "plugin")].find((r) => versionAt(r) !== null) ?? path.join(here, "..");
|
|
45690
|
+
var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
46030
45691
|
|
|
46031
45692
|
// server/src/sqlite.ts
|
|
46032
45693
|
var SCHEMA_GENERATION = 2;
|
|
@@ -46116,117 +45777,577 @@ function connectReader(file2 = dbFile()) {
|
|
|
46116
45777
|
return raw;
|
|
46117
45778
|
}
|
|
46118
45779
|
|
|
46119
|
-
// server/src/db.ts
|
|
46120
|
-
var JSON_COLUMNS = new Set(["next"]);
|
|
46121
|
-
var TOP_LEVEL = /^\$\[\d+\]\."([^"]+)"$/;
|
|
46122
|
-
var parseJson = new ParseJSONResultsPlugin({
|
|
46123
|
-
shouldParse: (_value, jsonPath) => JSON_COLUMNS.has(jsonPath.match(TOP_LEVEL)?.[1] ?? "")
|
|
46124
|
-
});
|
|
46125
|
-
function kyselyOn(connect) {
|
|
46126
|
-
return new Kysely({
|
|
46127
|
-
dialect: new SqliteDialect({ database: async () => adapt(connect()) }),
|
|
46128
|
-
plugins: [parseJson]
|
|
45780
|
+
// server/src/db.ts
|
|
45781
|
+
var JSON_COLUMNS = new Set(["next"]);
|
|
45782
|
+
var TOP_LEVEL = /^\$\[\d+\]\."([^"]+)"$/;
|
|
45783
|
+
var parseJson = new ParseJSONResultsPlugin({
|
|
45784
|
+
shouldParse: (_value, jsonPath) => JSON_COLUMNS.has(jsonPath.match(TOP_LEVEL)?.[1] ?? "")
|
|
45785
|
+
});
|
|
45786
|
+
function kyselyOn(connect) {
|
|
45787
|
+
return new Kysely({
|
|
45788
|
+
dialect: new SqliteDialect({ database: async () => adapt(connect()) }),
|
|
45789
|
+
plugins: [parseJson]
|
|
45790
|
+
});
|
|
45791
|
+
}
|
|
45792
|
+
function openReader(file2 = dbFile()) {
|
|
45793
|
+
return kyselyOn(() => connectReader(file2));
|
|
45794
|
+
}
|
|
45795
|
+
|
|
45796
|
+
// server/src/knowledge.ts
|
|
45797
|
+
var UNIT_KINDS = [
|
|
45798
|
+
"decision",
|
|
45799
|
+
"implementation",
|
|
45800
|
+
"finding",
|
|
45801
|
+
"dead_end",
|
|
45802
|
+
"question",
|
|
45803
|
+
"constraint"
|
|
45804
|
+
];
|
|
45805
|
+
var STANCES = ["do", "dont", "defer"];
|
|
45806
|
+
var LIFECYCLES = ["candidate", "active", "superseded", "withdrawn"];
|
|
45807
|
+
var OPTION_OUTCOMES = ["chosen", "rejected", "deferred", "proposed"];
|
|
45808
|
+
var EVIDENCE_ROLES = [
|
|
45809
|
+
"states",
|
|
45810
|
+
"proposes",
|
|
45811
|
+
"rejects",
|
|
45812
|
+
"explains",
|
|
45813
|
+
"implements",
|
|
45814
|
+
"reconsiders"
|
|
45815
|
+
];
|
|
45816
|
+
var WORK_STATUSES = ["active", "blocked", "paused", "done", "abandoned"];
|
|
45817
|
+
var FIELD_TYPES = ["text", "enum", "integer", "date"];
|
|
45818
|
+
var HOSTS = ["claude-code", "codex"];
|
|
45819
|
+
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
45820
|
+
|
|
45821
|
+
// server/src/anchors.ts
|
|
45822
|
+
import fs3 from "node:fs";
|
|
45823
|
+
import path3 from "node:path";
|
|
45824
|
+
var MAX_BYTES = 2 * 1024 * 1024;
|
|
45825
|
+
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path3.sep}`) || path3.isAbsolute(rel);
|
|
45826
|
+
var literal3 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45827
|
+
function readBytes(root, rel) {
|
|
45828
|
+
const abs = path3.join(root, rel);
|
|
45829
|
+
if (leaves(path3.relative(root, abs)))
|
|
45830
|
+
return;
|
|
45831
|
+
const st = fs3.lstatSync(abs, { throwIfNoEntry: false });
|
|
45832
|
+
if (!st)
|
|
45833
|
+
return null;
|
|
45834
|
+
if (!st.isFile() || st.size > MAX_BYTES)
|
|
45835
|
+
return;
|
|
45836
|
+
try {
|
|
45837
|
+
const inside = path3.relative(fs3.realpathSync(root), fs3.realpathSync(abs));
|
|
45838
|
+
if (leaves(inside))
|
|
45839
|
+
return;
|
|
45840
|
+
const buf = fs3.readFileSync(abs);
|
|
45841
|
+
return buf.includes(0) ? undefined : buf;
|
|
45842
|
+
} catch {
|
|
45843
|
+
return;
|
|
45844
|
+
}
|
|
45845
|
+
}
|
|
45846
|
+
function readText(root, rel) {
|
|
45847
|
+
const buf = readBytes(root, rel);
|
|
45848
|
+
return buf ? buf.toString("utf8") : buf;
|
|
45849
|
+
}
|
|
45850
|
+
function findSymbol(text, symbol2) {
|
|
45851
|
+
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`);
|
|
45852
|
+
const lines = text.split(/\r?\n/);
|
|
45853
|
+
return { lines, i: lines.findIndex((l) => re.test(l)) };
|
|
45854
|
+
}
|
|
45855
|
+
function fileState(root, rel) {
|
|
45856
|
+
if (!root)
|
|
45857
|
+
return "unknown";
|
|
45858
|
+
if (leaves(path3.relative(root, path3.join(root, rel))))
|
|
45859
|
+
return "unknown";
|
|
45860
|
+
let realRoot;
|
|
45861
|
+
try {
|
|
45862
|
+
realRoot = fs3.realpathSync(root);
|
|
45863
|
+
} catch {
|
|
45864
|
+
return "unknown";
|
|
45865
|
+
}
|
|
45866
|
+
let at = root;
|
|
45867
|
+
for (const part of rel.split("/")) {
|
|
45868
|
+
at = path3.join(at, part);
|
|
45869
|
+
let st;
|
|
45870
|
+
try {
|
|
45871
|
+
st = fs3.lstatSync(at, { throwIfNoEntry: false });
|
|
45872
|
+
} catch (e) {
|
|
45873
|
+
if (e.code === "ENOTDIR")
|
|
45874
|
+
return "gone";
|
|
45875
|
+
return "unknown";
|
|
45876
|
+
}
|
|
45877
|
+
if (!st)
|
|
45878
|
+
return "gone";
|
|
45879
|
+
if (st.isSymbolicLink()) {
|
|
45880
|
+
try {
|
|
45881
|
+
if (leaves(path3.relative(realRoot, fs3.realpathSync(at))))
|
|
45882
|
+
return "unknown";
|
|
45883
|
+
} catch {
|
|
45884
|
+
return "unknown";
|
|
45885
|
+
}
|
|
45886
|
+
}
|
|
45887
|
+
}
|
|
45888
|
+
return "present";
|
|
45889
|
+
}
|
|
45890
|
+
function checkAnchor(root, a) {
|
|
45891
|
+
if (!root)
|
|
45892
|
+
return { state: "unknown", line: null };
|
|
45893
|
+
const text = readText(root, a.path);
|
|
45894
|
+
if (text === null)
|
|
45895
|
+
return { state: "missing", line: null };
|
|
45896
|
+
if (text === undefined)
|
|
45897
|
+
return { state: "unknown", line: null };
|
|
45898
|
+
if (!a.symbol)
|
|
45899
|
+
return { state: "located", line: a.line_start };
|
|
45900
|
+
const { i } = findSymbol(text, a.symbol);
|
|
45901
|
+
if (i < 0)
|
|
45902
|
+
return { state: "missing", line: null };
|
|
45903
|
+
return {
|
|
45904
|
+
state: a.line_start === null || a.line_start === i + 1 ? "located" : "moved",
|
|
45905
|
+
line: i + 1
|
|
45906
|
+
};
|
|
45907
|
+
}
|
|
45908
|
+
|
|
45909
|
+
// server/src/git.ts
|
|
45910
|
+
import { execFileSync } from "node:child_process";
|
|
45911
|
+
function cleanGit(root, args, max = 1024 * 1024) {
|
|
45912
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_")));
|
|
45913
|
+
return execFileSync("git", ["-C", root, ...args], {
|
|
45914
|
+
env,
|
|
45915
|
+
maxBuffer: max,
|
|
45916
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
45917
|
+
timeout: 1e4
|
|
45918
|
+
});
|
|
45919
|
+
}
|
|
45920
|
+
|
|
45921
|
+
// server/src/record.ts
|
|
45922
|
+
var KEY = /^[a-z0-9][a-z0-9._/-]{0,63}$/;
|
|
45923
|
+
var FIELD_NAME = /^[a-z][a-z0-9_]{0,39}$/;
|
|
45924
|
+
var SOURCE_REF = /^s[1-9][0-9]{0,15}$/;
|
|
45925
|
+
var MAINTAINERS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
|
45926
|
+
var text = (max) => exports_external.string().trim().min(1).max(max);
|
|
45927
|
+
var Quote = exports_external.object({ source: exports_external.string().regex(SOURCE_REF, "cite a source ref such as s12"), quote: exports_external.string() }).strict();
|
|
45928
|
+
var Evidence = Quote.extend({
|
|
45929
|
+
role: exports_external.enum(EVIDENCE_ROLES).exclude(["reconsiders"]),
|
|
45930
|
+
reported_speaker: text(100).optional()
|
|
45931
|
+
}).strict();
|
|
45932
|
+
var Unit = exports_external.object({
|
|
45933
|
+
key: exports_external.string().regex(KEY, "use lowercase letters, digits, and . _ - / (at most 64)"),
|
|
45934
|
+
kind: exports_external.enum(UNIT_KINDS),
|
|
45935
|
+
stance: exports_external.enum(STANCES).optional(),
|
|
45936
|
+
text: text(2000),
|
|
45937
|
+
why: text(2000).optional(),
|
|
45938
|
+
scope_note: text(1000).optional(),
|
|
45939
|
+
revisit_when: text(1000).optional(),
|
|
45940
|
+
no_code_surface: text(500).optional(),
|
|
45941
|
+
options: exports_external.array(exports_external.object({
|
|
45942
|
+
text: text(500),
|
|
45943
|
+
outcome: exports_external.enum(OPTION_OUTCOMES),
|
|
45944
|
+
why: text(2000).optional(),
|
|
45945
|
+
reconsider_when: text(1000).optional(),
|
|
45946
|
+
reconsider_quote: Quote.optional(),
|
|
45947
|
+
evidence: exports_external.array(Evidence.partial({ role: true })).max(10).default([])
|
|
45948
|
+
}).strict()).max(12).default([]),
|
|
45949
|
+
evidence: exports_external.array(Evidence).max(20).default([]),
|
|
45950
|
+
adoption: exports_external.array(Quote).max(5).default([]),
|
|
45951
|
+
anchors: exports_external.array(exports_external.object({
|
|
45952
|
+
path: text(500),
|
|
45953
|
+
symbol: text(200).optional(),
|
|
45954
|
+
role: exports_external.enum(["applies_to", "evidence"]),
|
|
45955
|
+
commit: exports_external.string().regex(/^[0-9a-f]{40}$/, "a full 40-character commit sha").optional(),
|
|
45956
|
+
lines: exports_external.tuple([exports_external.number().int().positive(), exports_external.number().int().positive()]).optional()
|
|
45957
|
+
}).strict()).max(20).default([]),
|
|
45958
|
+
aliases: exports_external.array(exports_external.string()).max(12).default([]),
|
|
45959
|
+
supersedes: exports_external.string().min(1).optional(),
|
|
45960
|
+
conflicts: exports_external.array(exports_external.string().min(1)).max(5).default([]),
|
|
45961
|
+
fields: exports_external.array(exports_external.object({
|
|
45962
|
+
name: exports_external.string().regex(FIELD_NAME, "a defined field name"),
|
|
45963
|
+
value: text(200),
|
|
45964
|
+
quote: Quote
|
|
45965
|
+
}).strict()).max(12).default([])
|
|
45966
|
+
}).strict();
|
|
45967
|
+
var FieldDef = exports_external.object({
|
|
45968
|
+
name: exports_external.string().regex(FIELD_NAME, "a lowercase letter, then lowercase letters, digits, and _ (at most 40)"),
|
|
45969
|
+
type: exports_external.enum(FIELD_TYPES),
|
|
45970
|
+
label: text(100),
|
|
45971
|
+
description: text(500),
|
|
45972
|
+
enum: exports_external.array(text(100)).min(1).max(30).optional(),
|
|
45973
|
+
kinds: exports_external.array(exports_external.enum(UNIT_KINDS)).max(UNIT_KINDS.length).default([]),
|
|
45974
|
+
quote: Quote
|
|
45975
|
+
}).strict();
|
|
45976
|
+
var Work = exports_external.object({
|
|
45977
|
+
key: exports_external.string().regex(KEY),
|
|
45978
|
+
title: text(200),
|
|
45979
|
+
goal: text(1000),
|
|
45980
|
+
current: text(2000),
|
|
45981
|
+
next: exports_external.array(text(500)).max(10).default([]),
|
|
45982
|
+
status: exports_external.enum(WORK_STATUSES)
|
|
45983
|
+
}).strict();
|
|
45984
|
+
var Record = exports_external.object({
|
|
45985
|
+
field_defs: exports_external.array(FieldDef).max(10).default([]),
|
|
45986
|
+
units: exports_external.array(Unit).max(50),
|
|
45987
|
+
work: Work.optional()
|
|
45988
|
+
}).strict();
|
|
45989
|
+
function repoPath(p) {
|
|
45990
|
+
const s = p.trim().replace(/^\.\//, "");
|
|
45991
|
+
if (!s || s.startsWith("/") || s.includes("\\") || /^[A-Za-z]:/.test(s) || /\p{Cc}/u.test(s))
|
|
45992
|
+
return null;
|
|
45993
|
+
const parts = s.split("/");
|
|
45994
|
+
if (parts.some((x) => x === ".." || x === "." || x === ""))
|
|
45995
|
+
return null;
|
|
45996
|
+
return s;
|
|
45997
|
+
}
|
|
45998
|
+
|
|
45999
|
+
// server/src/search.ts
|
|
46000
|
+
var UNIT_PAGE = 200;
|
|
46001
|
+
var UNIT_SCAN_MAX = 2000;
|
|
46002
|
+
var SOURCE_PAGE = 50;
|
|
46003
|
+
var SOURCE_SCAN_MAX = 600;
|
|
46004
|
+
var SOURCE_SCAN_BYTES = 64 * 1024 * 1024;
|
|
46005
|
+
var LIFE_ORDER = { active: 0, candidate: 1, superseded: 2, withdrawn: 3 };
|
|
46006
|
+
var strong = (matched, of) => matched * 2 > of;
|
|
46007
|
+
var PATH_REFUSED = "path must be relative to the repository root, such as src/x.ts: absolute paths, .., backslashes, and empty parts are refused";
|
|
46008
|
+
async function searchUnits(db, projectId, q) {
|
|
46009
|
+
const wanted = queryTerms(q.question);
|
|
46010
|
+
const path4 = q.path === undefined ? undefined : repoPath(q.path);
|
|
46011
|
+
if (path4 === null)
|
|
46012
|
+
return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0, refused: PATH_REFUSED };
|
|
46013
|
+
const match = ftsQuery(q.question);
|
|
46014
|
+
if (!match)
|
|
46015
|
+
return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0 };
|
|
46016
|
+
let query = db.selectFrom(sql`(select unit_fts.rowid as rowid, bm25(unit_fts, 3, 2, 1) as rank from unit_fts cross join unit on unit.id = unit_fts.rowid where unit_fts match ${match} and unit.project_id = ${projectId})`.as("f")).innerJoin("unit as u", "u.id", "f.rowid").where("u.project_id", "=", projectId).where("u.extraction", "=", "supported");
|
|
46017
|
+
if (q.kinds?.length)
|
|
46018
|
+
query = query.where("u.kind", "in", q.kinds);
|
|
46019
|
+
if (q.lifecycles?.length)
|
|
46020
|
+
query = query.where("u.lifecycle", "in", q.lifecycles);
|
|
46021
|
+
if (path4)
|
|
46022
|
+
query = query.where(({ exists, selectFrom }) => exists(selectFrom("unit_anchor as a").select("a.id").whereRef("a.unit_id", "=", "u.id").where("a.path", "=", path4).where("a.retired_at", "is", null)));
|
|
46023
|
+
const ranked = await query.select(["u.id", "f.rank"]).orderBy("f.rank").orderBy("u.id").limit(UNIT_SCAN_MAX + 1).execute();
|
|
46024
|
+
const stopped = ranked.length > UNIT_SCAN_MAX;
|
|
46025
|
+
let weaker = 0;
|
|
46026
|
+
let read = 0;
|
|
46027
|
+
const hits = [];
|
|
46028
|
+
for (let at = 0;at < Math.min(ranked.length, UNIT_SCAN_MAX); at += UNIT_PAGE) {
|
|
46029
|
+
const part = ranked.slice(at, Math.min(at + UNIT_PAGE, UNIT_SCAN_MAX));
|
|
46030
|
+
const order = new Map(part.map((r, i) => [r.id, i]));
|
|
46031
|
+
const rows = (await query.select([
|
|
46032
|
+
"u.id",
|
|
46033
|
+
"u.key",
|
|
46034
|
+
"u.kind",
|
|
46035
|
+
"u.stance",
|
|
46036
|
+
"u.lifecycle",
|
|
46037
|
+
"u.text",
|
|
46038
|
+
"u.why",
|
|
46039
|
+
"u.scope_note",
|
|
46040
|
+
"u.revisit_when",
|
|
46041
|
+
"u.content_hash",
|
|
46042
|
+
"f.rank"
|
|
46043
|
+
]).where("u.id", "in", part.map((r) => r.id)).execute()).sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
|
|
46044
|
+
read += rows.length;
|
|
46045
|
+
weaker += await judgeUnits(db, rows, wanted, hits);
|
|
46046
|
+
}
|
|
46047
|
+
const shown = new Set(hits.map((h) => h.id));
|
|
46048
|
+
for (const h of [...hits].filter((x) => x.lifecycle === "superseded"))
|
|
46049
|
+
for (const n of await liveSuccessors(db, h.id))
|
|
46050
|
+
if (!shown.has(n.id) && (!q.kinds?.length || q.kinds.includes(n.kind)) && (!q.lifecycles?.length || q.lifecycles.includes(n.lifecycle))) {
|
|
46051
|
+
shown.add(n.id);
|
|
46052
|
+
hits.push({
|
|
46053
|
+
...n,
|
|
46054
|
+
options: [],
|
|
46055
|
+
anchors: [],
|
|
46056
|
+
matched: [],
|
|
46057
|
+
aliasOnly: false,
|
|
46058
|
+
successorOf: h.key,
|
|
46059
|
+
rank: h.rank
|
|
46060
|
+
});
|
|
46061
|
+
}
|
|
46062
|
+
hits.sort((a, b) => (LIFE_ORDER[a.lifecycle] ?? 9) - (LIFE_ORDER[b.lifecycle] ?? 9) || b.matched.length - a.matched.length || a.rank - b.rank);
|
|
46063
|
+
return {
|
|
46064
|
+
hits: hits.slice(0, q.limit).map(({ rank: _rank, ...h }) => h),
|
|
46065
|
+
weaker,
|
|
46066
|
+
terms: wanted,
|
|
46067
|
+
stopped,
|
|
46068
|
+
read
|
|
46069
|
+
};
|
|
46070
|
+
}
|
|
46071
|
+
async function liveSuccessors(db, id) {
|
|
46072
|
+
const seen = new Set([id]);
|
|
46073
|
+
const found = [];
|
|
46074
|
+
const replaced = new Set;
|
|
46075
|
+
for (let frontier = [id];frontier.length; ) {
|
|
46076
|
+
const next = await db.selectFrom("unit_link as l").innerJoin("unit as n", "n.id", "l.from_unit").where("l.to_unit", "in", frontier).where("l.kind", "=", "supersedes").where("n.extraction", "=", "supported").where("n.lifecycle", "<>", "candidate").select([
|
|
46077
|
+
"l.to_unit",
|
|
46078
|
+
"n.id",
|
|
46079
|
+
"n.key",
|
|
46080
|
+
"n.kind",
|
|
46081
|
+
"n.stance",
|
|
46082
|
+
"n.lifecycle",
|
|
46083
|
+
"n.text",
|
|
46084
|
+
"n.why",
|
|
46085
|
+
"n.revisit_when"
|
|
46086
|
+
]).execute();
|
|
46087
|
+
frontier = [];
|
|
46088
|
+
for (const { to_unit, ...n } of next) {
|
|
46089
|
+
replaced.add(to_unit);
|
|
46090
|
+
if (seen.has(n.id))
|
|
46091
|
+
continue;
|
|
46092
|
+
seen.add(n.id);
|
|
46093
|
+
found.push(n);
|
|
46094
|
+
frontier.push(n.id);
|
|
46095
|
+
}
|
|
46096
|
+
}
|
|
46097
|
+
return found.filter((n) => !replaced.has(n.id));
|
|
46098
|
+
}
|
|
46099
|
+
async function judgeUnits(db, rows, wanted, hits) {
|
|
46100
|
+
const ids = rows.map((r) => r.id);
|
|
46101
|
+
const [options, anchors, aliases, fields] = ids.length ? await Promise.all([
|
|
46102
|
+
db.selectFrom("unit_option").select(["unit_id", "text", "outcome", "why", "reconsider_when"]).where("unit_id", "in", ids).orderBy("position").execute(),
|
|
46103
|
+
db.selectFrom("unit_anchor").select(["unit_id", "path", "symbol", "role"]).where("unit_id", "in", ids).where("retired_at", "is", null).orderBy("id").execute(),
|
|
46104
|
+
db.selectFrom("unit_alias").select(["unit_id", "terms", "content_hash"]).where("unit_id", "in", ids).orderBy("id", "desc").execute(),
|
|
46105
|
+
db.selectFrom("unit_field as f").innerJoin("field_def as d", "d.id", "f.field_def_id").select(["f.unit_id", "d.name", "f.value"]).where("f.unit_id", "in", ids).execute()
|
|
46106
|
+
]) : [[], [], [], []];
|
|
46107
|
+
let weaker = 0;
|
|
46108
|
+
for (const r of rows) {
|
|
46109
|
+
const opts = options.filter((o) => o.unit_id === r.id);
|
|
46110
|
+
const anch = anchors.filter((a) => a.unit_id === r.id);
|
|
46111
|
+
const alias = aliases.find((a) => a.unit_id === r.id && Buffer.from(a.content_hash).equals(Buffer.from(r.content_hash)));
|
|
46112
|
+
const own2 = new Set(terms([
|
|
46113
|
+
r.text,
|
|
46114
|
+
r.why,
|
|
46115
|
+
r.scope_note,
|
|
46116
|
+
r.revisit_when,
|
|
46117
|
+
...opts.flatMap((o) => [o.text, o.why, o.reconsider_when]),
|
|
46118
|
+
...anch.flatMap((a) => [a.path, a.symbol]),
|
|
46119
|
+
...fields.filter((f) => f.unit_id === r.id).flatMap((f) => [f.name, f.value])
|
|
46120
|
+
].filter(Boolean).join(`
|
|
46121
|
+
`)));
|
|
46122
|
+
const extra = new Set(alias ? terms(JSON.parse(alias.terms).join(" ")) : []);
|
|
46123
|
+
const matched = wanted.filter((w) => own2.has(w) || extra.has(w));
|
|
46124
|
+
const ident = new Set(anch.flatMap((a) => [a.path, a.symbol].flatMap((x) => x ? [identTerm(x)] : [])));
|
|
46125
|
+
if (!strong(matched.length, wanted.length) && !matched.some((w) => ident.has(w))) {
|
|
46126
|
+
weaker++;
|
|
46127
|
+
continue;
|
|
46128
|
+
}
|
|
46129
|
+
hits.push({
|
|
46130
|
+
id: r.id,
|
|
46131
|
+
key: r.key,
|
|
46132
|
+
kind: r.kind,
|
|
46133
|
+
stance: r.stance,
|
|
46134
|
+
lifecycle: r.lifecycle,
|
|
46135
|
+
text: r.text,
|
|
46136
|
+
why: r.why,
|
|
46137
|
+
revisit_when: r.revisit_when,
|
|
46138
|
+
options: opts.map((o) => ({ text: o.text, outcome: o.outcome, why: o.why })),
|
|
46139
|
+
anchors: anch.map((a) => ({ path: a.path, symbol: a.symbol, role: a.role })),
|
|
46140
|
+
matched,
|
|
46141
|
+
aliasOnly: matched.every((w) => !own2.has(w)),
|
|
46142
|
+
rank: r.rank
|
|
46143
|
+
});
|
|
46144
|
+
}
|
|
46145
|
+
return weaker;
|
|
46146
|
+
}
|
|
46147
|
+
async function searchSources(db, projectId, question, limit, owner) {
|
|
46148
|
+
const wanted = queryTerms(question);
|
|
46149
|
+
const match = ftsQuery(question);
|
|
46150
|
+
if (!match)
|
|
46151
|
+
return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0 };
|
|
46152
|
+
const query = db.selectFrom(sql`(select source_fts.rowid as rowid, bm25(source_fts) as rank from source_fts cross join source on source.id = source_fts.rowid where source_fts match ${match} and source.project_id = ${projectId})`.as("f")).innerJoin("source as s", "s.id", "f.rowid").where("s.project_id", "=", projectId).$if(owner !== undefined, (q) => q.where("s.kind", "=", "session_message").where("s.author_kind", "=", "owner").$if((owner?.notSessions.length ?? 0) > 0, (q2) => q2.where("s.session_id", "not in", owner?.notSessions ?? [])));
|
|
46153
|
+
const ranked = await query.select(["s.id", "f.rank"]).orderBy("f.rank").orderBy("s.id").limit(SOURCE_SCAN_MAX + 1).execute();
|
|
46154
|
+
let weaker = 0;
|
|
46155
|
+
let stopped = false;
|
|
46156
|
+
const hits = [];
|
|
46157
|
+
let read = 0;
|
|
46158
|
+
let bytes2 = 0;
|
|
46159
|
+
scan:
|
|
46160
|
+
for (let at = 0;at < ranked.length; at += SOURCE_PAGE) {
|
|
46161
|
+
const ids = ranked.slice(at, at + SOURCE_PAGE).map((r) => r.id);
|
|
46162
|
+
const byId = new Map((await db.selectFrom("source").select([
|
|
46163
|
+
"id",
|
|
46164
|
+
"kind",
|
|
46165
|
+
"artifact",
|
|
46166
|
+
"author_kind",
|
|
46167
|
+
"author_login",
|
|
46168
|
+
"created_at",
|
|
46169
|
+
"text",
|
|
46170
|
+
"session_id",
|
|
46171
|
+
"turn_id"
|
|
46172
|
+
]).where("id", "in", ids).execute()).map((r) => [r.id, r]));
|
|
46173
|
+
for (const id of ids) {
|
|
46174
|
+
if (read >= SOURCE_SCAN_MAX || bytes2 >= SOURCE_SCAN_BYTES) {
|
|
46175
|
+
stopped = true;
|
|
46176
|
+
break scan;
|
|
46177
|
+
}
|
|
46178
|
+
const r = byId.get(id);
|
|
46179
|
+
if (!r)
|
|
46180
|
+
continue;
|
|
46181
|
+
read++;
|
|
46182
|
+
bytes2 += Buffer.byteLength(r.text);
|
|
46183
|
+
const own2 = new Set(terms(r.text));
|
|
46184
|
+
const matched = wanted.filter((w) => own2.has(w));
|
|
46185
|
+
if (!strong(matched.length, wanted.length)) {
|
|
46186
|
+
weaker++;
|
|
46187
|
+
continue;
|
|
46188
|
+
}
|
|
46189
|
+
hits.push({
|
|
46190
|
+
id: r.id,
|
|
46191
|
+
kind: r.kind,
|
|
46192
|
+
artifact: r.artifact,
|
|
46193
|
+
author: r.author_login ?? r.author_kind,
|
|
46194
|
+
created_at: r.created_at,
|
|
46195
|
+
text: r.text,
|
|
46196
|
+
matched,
|
|
46197
|
+
session: r.session_id,
|
|
46198
|
+
turn: r.turn_id
|
|
46199
|
+
});
|
|
46200
|
+
if (hits.length >= limit)
|
|
46201
|
+
break scan;
|
|
46202
|
+
}
|
|
46203
|
+
}
|
|
46204
|
+
if (!stopped && hits.length < limit && ranked.length > SOURCE_SCAN_MAX)
|
|
46205
|
+
stopped = true;
|
|
46206
|
+
return { hits, weaker, terms: wanted, stopped, read };
|
|
46207
|
+
}
|
|
46208
|
+
|
|
46209
|
+
// server/src/asked.ts
|
|
46210
|
+
var DECIDED_KINDS = new Set(["decision", "constraint"]);
|
|
46211
|
+
var DECIDED_LIFECYCLES = new Set(["active", "superseded", "withdrawn"]);
|
|
46212
|
+
var PER_MESSAGE = 10;
|
|
46213
|
+
async function askedBefore(db, projectId, q) {
|
|
46214
|
+
const kept = (u) => (!q.kinds?.length || q.kinds.includes(u.kind)) && (!q.lifecycles?.length || q.lifecycles.includes(u.lifecycle));
|
|
46215
|
+
const found = await searchSources(db, projectId, q.question, Number.POSITIVE_INFINITY, {
|
|
46216
|
+
notSessions: q.notSessions
|
|
46129
46217
|
});
|
|
46218
|
+
const ids = found.hits.map((h) => h.id);
|
|
46219
|
+
const cited = ids.length ? await citing(db, ids) : [];
|
|
46220
|
+
const traced = new Set(ids.length ? (await db.selectFrom("source_processing").select("source_id").where("source_id", "in", ids).execute()).map((r) => r.source_id) : []);
|
|
46221
|
+
const decided = new Set(cited.filter((c) => DECIDED_KINDS.has(c.kind) && DECIDED_LIFECYCLES.has(c.lifecycle)).map((c) => c.source_id));
|
|
46222
|
+
const all = [];
|
|
46223
|
+
for (const m of found.hits) {
|
|
46224
|
+
const direct = unique(cited.filter((c) => c.source_id === m.id));
|
|
46225
|
+
const shown = direct.filter(kept);
|
|
46226
|
+
all.push({
|
|
46227
|
+
message: m,
|
|
46228
|
+
led: shown.map((u) => ({ ...u, now: [] })),
|
|
46229
|
+
hidden: direct.length - shown.length,
|
|
46230
|
+
context: [],
|
|
46231
|
+
direct: new Set(direct.map((u) => u.id)),
|
|
46232
|
+
decided: decided.has(m.id),
|
|
46233
|
+
traced: traced.has(m.id),
|
|
46234
|
+
reply: null
|
|
46235
|
+
});
|
|
46236
|
+
}
|
|
46237
|
+
const messages = all.slice(0, q.limit);
|
|
46238
|
+
for (const e of messages) {
|
|
46239
|
+
for (const u of e.led)
|
|
46240
|
+
if (u.lifecycle === "superseded")
|
|
46241
|
+
u.now = await liveSuccessors(db, u.id);
|
|
46242
|
+
await sameTurn(db, e, kept);
|
|
46243
|
+
}
|
|
46244
|
+
const sessions = new Map;
|
|
46245
|
+
for (const e of all) {
|
|
46246
|
+
const s = e.message.session ?? "";
|
|
46247
|
+
const was = sessions.get(s) ?? { decided: false, traced: true };
|
|
46248
|
+
sessions.set(s, { decided: was.decided || e.decided, traced: was.traced && e.traced });
|
|
46249
|
+
}
|
|
46250
|
+
const none = [...sessions.values()].every((s) => !s.decided);
|
|
46251
|
+
const repeats = sessions.size >= 2 && none ? {
|
|
46252
|
+
untraced: [...sessions].filter(([, s]) => !s.traced).map(([id]) => id),
|
|
46253
|
+
traced: [...sessions].filter(([, s]) => s.traced).map(([id]) => id)
|
|
46254
|
+
} : null;
|
|
46255
|
+
return {
|
|
46256
|
+
messages,
|
|
46257
|
+
repeats,
|
|
46258
|
+
terms: found.terms,
|
|
46259
|
+
weaker: found.weaker,
|
|
46260
|
+
stopped: found.stopped,
|
|
46261
|
+
read: found.read
|
|
46262
|
+
};
|
|
46130
46263
|
}
|
|
46131
|
-
function
|
|
46132
|
-
|
|
46264
|
+
async function citing(db, sources) {
|
|
46265
|
+
const [evidence, adoption] = await Promise.all([
|
|
46266
|
+
db.selectFrom("unit_evidence as e").innerJoin("unit as u", "u.id", "e.unit_id").where("e.source_id", "in", sources).where("e.retracted_at", "is", null).where("u.extraction", "=", "supported").select(["e.source_id", "e.role", "u.id", "u.key", "u.kind", "u.lifecycle"]).orderBy("u.id").execute(),
|
|
46267
|
+
db.selectFrom("unit_adoption as a").innerJoin("unit as u", "u.id", "a.unit_id").where("a.source_id", "in", sources).where("a.retracted_at", "is", null).where("u.extraction", "=", "supported").select(["a.source_id", "u.id", "u.key", "u.kind", "u.lifecycle"]).orderBy("u.id").execute()
|
|
46268
|
+
]);
|
|
46269
|
+
return [...evidence, ...adoption.map((a) => ({ ...a, role: "adoption" }))];
|
|
46133
46270
|
}
|
|
46134
|
-
|
|
46135
|
-
|
|
46136
|
-
|
|
46137
|
-
|
|
46138
|
-
|
|
46139
|
-
// server/src/anchors.ts
|
|
46140
|
-
import fs3 from "node:fs";
|
|
46141
|
-
import path3 from "node:path";
|
|
46142
|
-
var MAX_BYTES = 2 * 1024 * 1024;
|
|
46143
|
-
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path3.sep}`) || path3.isAbsolute(rel);
|
|
46144
|
-
var literal3 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46145
|
-
function readBytes(root, rel) {
|
|
46146
|
-
const abs = path3.join(root, rel);
|
|
46147
|
-
if (leaves(path3.relative(root, abs)))
|
|
46148
|
-
return;
|
|
46149
|
-
const st = fs3.lstatSync(abs, { throwIfNoEntry: false });
|
|
46150
|
-
if (!st)
|
|
46151
|
-
return null;
|
|
46152
|
-
if (!st.isFile() || st.size > MAX_BYTES)
|
|
46271
|
+
var unique = (xs) => [...new Map(xs.map((x) => [x.id, x])).values()];
|
|
46272
|
+
async function sameTurn(db, e, kept) {
|
|
46273
|
+
const { session, turn, id } = e.message;
|
|
46274
|
+
if (!session || !turn)
|
|
46153
46275
|
return;
|
|
46154
|
-
|
|
46155
|
-
|
|
46156
|
-
|
|
46157
|
-
return;
|
|
46158
|
-
const buf = fs3.readFileSync(abs);
|
|
46159
|
-
return buf.includes(0) ? undefined : buf;
|
|
46160
|
-
} catch {
|
|
46276
|
+
const others = await db.selectFrom("source").select(["id", "author_kind"]).where("session_id", "=", session).where("turn_id", "=", turn).where("id", "<>", id).orderBy("id").execute();
|
|
46277
|
+
e.reply = others.findLast((o) => o.author_kind === "assistant")?.id ?? null;
|
|
46278
|
+
if (!others.length)
|
|
46161
46279
|
return;
|
|
46280
|
+
const speaker = new Map(others.map((o) => [o.id, o.author_kind]));
|
|
46281
|
+
const seen = new Set;
|
|
46282
|
+
for (const c of await citing(db, others.map((o) => o.id))) {
|
|
46283
|
+
const k = `${c.id}\x00${c.source_id}\x00${c.role}`;
|
|
46284
|
+
if (e.direct.has(c.id) || !kept(c) || seen.has(k))
|
|
46285
|
+
continue;
|
|
46286
|
+
seen.add(k);
|
|
46287
|
+
e.context.push({
|
|
46288
|
+
key: c.key,
|
|
46289
|
+
kind: c.kind,
|
|
46290
|
+
lifecycle: c.lifecycle,
|
|
46291
|
+
speaker: speaker.get(c.source_id) ?? "",
|
|
46292
|
+
role: c.role
|
|
46293
|
+
});
|
|
46162
46294
|
}
|
|
46163
46295
|
}
|
|
46164
|
-
|
|
46165
|
-
|
|
46166
|
-
|
|
46167
|
-
|
|
46168
|
-
|
|
46169
|
-
|
|
46170
|
-
|
|
46171
|
-
|
|
46172
|
-
}
|
|
46173
|
-
function fileState(root, rel) {
|
|
46174
|
-
if (!root)
|
|
46175
|
-
return "unknown";
|
|
46176
|
-
if (leaves(path3.relative(root, path3.join(root, rel))))
|
|
46177
|
-
return "unknown";
|
|
46178
|
-
let realRoot;
|
|
46179
|
-
try {
|
|
46180
|
-
realRoot = fs3.realpathSync(root);
|
|
46181
|
-
} catch {
|
|
46182
|
-
return "unknown";
|
|
46183
|
-
}
|
|
46184
|
-
let at = root;
|
|
46185
|
-
for (const part of rel.split("/")) {
|
|
46186
|
-
at = path3.join(at, part);
|
|
46187
|
-
let st;
|
|
46188
|
-
try {
|
|
46189
|
-
st = fs3.lstatSync(at, { throwIfNoEntry: false });
|
|
46190
|
-
} catch (e) {
|
|
46191
|
-
if (e.code === "ENOTDIR")
|
|
46192
|
-
return "gone";
|
|
46193
|
-
return "unknown";
|
|
46296
|
+
var UNKNOWN_SESSION = "Current session unknown; results and session counts may include its messages. Pass session to exclude it.";
|
|
46297
|
+
function askedText(r, known = true) {
|
|
46298
|
+
const parts = r.messages.map((e) => {
|
|
46299
|
+
const m = e.message;
|
|
46300
|
+
const lines = [`## s${m.id}: ${inline(m.artifact)}, ${m.created_at}`, inline(head(m.text, 600))];
|
|
46301
|
+
if (e.led.length) {
|
|
46302
|
+
lines.push("Led to:");
|
|
46303
|
+
for (const u of e.led.slice(0, PER_MESSAGE))
|
|
46304
|
+
lines.push(`- ${inline(u.key)} (${u.kind}, ${u.lifecycle}${u.now.length ? `; now ${u.now.map((n) => `${inline(n.key)} (${n.lifecycle})`).join(", ")}` : ""})`);
|
|
46194
46305
|
}
|
|
46195
|
-
if (
|
|
46196
|
-
|
|
46197
|
-
if (
|
|
46198
|
-
|
|
46199
|
-
|
|
46200
|
-
|
|
46201
|
-
|
|
46202
|
-
|
|
46203
|
-
|
|
46306
|
+
if (e.led.length > PER_MESSAGE)
|
|
46307
|
+
lines.push(`and ${e.led.length - PER_MESSAGE} more tied records`);
|
|
46308
|
+
if (e.hidden)
|
|
46309
|
+
lines.push(`${e.hidden} tied record${e.hidden === 1 ? "" : "s"} hidden by the filters.`);
|
|
46310
|
+
if (!e.decided)
|
|
46311
|
+
lines.push(`No recorded decision.${e.traced ? "" : ` Not traced yet: run /sphica:trace ${inline(m.session ?? "")}.`}`);
|
|
46312
|
+
if (e.context.length) {
|
|
46313
|
+
lines.push("Same turn (context, not necessarily the answer):");
|
|
46314
|
+
for (const c of e.context.slice(0, PER_MESSAGE))
|
|
46315
|
+
lines.push(`- ${inline(c.key)} (${c.kind}, ${c.lifecycle}): quotes the ${c.speaker} (${c.role})`);
|
|
46316
|
+
if (e.context.length > PER_MESSAGE)
|
|
46317
|
+
lines.push(`and ${e.context.length - PER_MESSAGE} more in the same turn`);
|
|
46204
46318
|
}
|
|
46319
|
+
if (e.reply !== null)
|
|
46320
|
+
lines.push(`Reply: s${e.reply} (read it with read)`);
|
|
46321
|
+
return lines.join(`
|
|
46322
|
+
`);
|
|
46323
|
+
});
|
|
46324
|
+
const out = [`${known ? "Earlier owner" : "Owner"} messages matching: ${r.terms.join(", ")}`, ...parts];
|
|
46325
|
+
if (r.repeats) {
|
|
46326
|
+
const n = r.repeats.untraced.length + r.repeats.traced.length;
|
|
46327
|
+
const within = r.stopped ? ` (within the first ${r.read} candidates by rank)` : "";
|
|
46328
|
+
out.push([
|
|
46329
|
+
known ? `Asked in ${n} sessions with no recorded decision${within}:` : `Matching messages in ${n} sessions with no recorded decision (current session may be included)${within}:`,
|
|
46330
|
+
...r.repeats.untraced.length ? [`- not traced yet: ${r.repeats.untraced.map(inline).join(", ")} (run /sphica:trace with each)`] : [],
|
|
46331
|
+
...r.repeats.traced.length ? [`- traced: ${r.repeats.traced.map(inline).join(", ")}`] : []
|
|
46332
|
+
].join(`
|
|
46333
|
+
`));
|
|
46205
46334
|
}
|
|
46206
|
-
|
|
46207
|
-
}
|
|
46208
|
-
|
|
46209
|
-
|
|
46210
|
-
|
|
46211
|
-
|
|
46212
|
-
|
|
46213
|
-
return { state: "missing", line: null };
|
|
46214
|
-
if (text === undefined)
|
|
46215
|
-
return { state: "unknown", line: null };
|
|
46216
|
-
if (!a.symbol)
|
|
46217
|
-
return { state: "located", line: a.line_start };
|
|
46218
|
-
const { i } = findSymbol(text, a.symbol);
|
|
46219
|
-
if (i < 0)
|
|
46220
|
-
return { state: "missing", line: null };
|
|
46221
|
-
return {
|
|
46222
|
-
state: a.line_start === null || a.line_start === i + 1 ? "located" : "moved",
|
|
46223
|
-
line: i + 1
|
|
46224
|
-
};
|
|
46335
|
+
if (r.stopped)
|
|
46336
|
+
out.push(`Stopped after ${r.read} candidates by rank; more may match.`);
|
|
46337
|
+
if (!known)
|
|
46338
|
+
out.push(UNKNOWN_SESSION);
|
|
46339
|
+
return out.join(`
|
|
46340
|
+
|
|
46341
|
+
`);
|
|
46225
46342
|
}
|
|
46226
46343
|
|
|
46344
|
+
// server/src/export.ts
|
|
46345
|
+
import fs5 from "node:fs";
|
|
46346
|
+
import path5 from "node:path";
|
|
46347
|
+
|
|
46227
46348
|
// server/src/read.ts
|
|
46228
46349
|
var UNSUPPORTED = "unsupported: its owner quote was retracted or forgotten, so it is not the owner's condition";
|
|
46229
|
-
var cut = (
|
|
46350
|
+
var cut = (text2, start, end) => Buffer.from(text2, "utf8").subarray(start, end).toString("utf8");
|
|
46230
46351
|
var speaker = (s) => s.author_kind === "owner" ? "the owner" : s.author_kind === "assistant" ? "the assistant" : `${s.author_login ?? "someone"} (${s.author_association ?? s.author_kind})`;
|
|
46231
46352
|
async function readUnit(db, projectId, ref, root, asOf) {
|
|
46232
46353
|
const byId = /^u([1-9][0-9]{0,15})$/.exec(ref);
|
|
@@ -46375,8 +46496,8 @@ async function readSource(db, projectId, ref) {
|
|
|
46375
46496
|
`);
|
|
46376
46497
|
}
|
|
46377
46498
|
var PART = 64 * 1024;
|
|
46378
|
-
function part(id,
|
|
46379
|
-
const all = Buffer.from(
|
|
46499
|
+
function part(id, text2, from) {
|
|
46500
|
+
const all = Buffer.from(text2, "utf8");
|
|
46380
46501
|
let start = Math.min(from, all.length);
|
|
46381
46502
|
while (start > 0 && start < all.length && ((all[start] ?? 0) & 192) === 128)
|
|
46382
46503
|
start--;
|
|
@@ -46388,20 +46509,6 @@ function part(id, text, from) {
|
|
|
46388
46509
|
// server/src/rule-files.ts
|
|
46389
46510
|
import fs4 from "node:fs";
|
|
46390
46511
|
import path4 from "node:path";
|
|
46391
|
-
|
|
46392
|
-
// server/src/git.ts
|
|
46393
|
-
import { execFileSync } from "node:child_process";
|
|
46394
|
-
function cleanGit(root, args, max = 1024 * 1024) {
|
|
46395
|
-
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_")));
|
|
46396
|
-
return execFileSync("git", ["-C", root, ...args], {
|
|
46397
|
-
env,
|
|
46398
|
-
maxBuffer: max,
|
|
46399
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
46400
|
-
timeout: 1e4
|
|
46401
|
-
});
|
|
46402
|
-
}
|
|
46403
|
-
|
|
46404
|
-
// server/src/rule-files.ts
|
|
46405
46512
|
var RULE_LIMITS = { files: 200, bytes: 256 * 1024, depth: 8, entries: 5000 };
|
|
46406
46513
|
var RULE_NAMES = new Set(["CLAUDE.md", "AGENTS.md", "AGENTS.override.md"]);
|
|
46407
46514
|
var PATHSPECS = [...RULE_NAMES, ".claude/rules/**/*.md"].map((p) => `:(glob)**/${p}`);
|
|
@@ -46422,13 +46529,13 @@ function ruleFiles(root) {
|
|
|
46422
46529
|
out.skipped++;
|
|
46423
46530
|
continue;
|
|
46424
46531
|
}
|
|
46425
|
-
const
|
|
46426
|
-
if (
|
|
46532
|
+
const text2 = readBounded(root, realRoot, rel);
|
|
46533
|
+
if (text2 === null)
|
|
46427
46534
|
continue;
|
|
46428
|
-
if (
|
|
46535
|
+
if (text2 === undefined)
|
|
46429
46536
|
out.skipped++;
|
|
46430
46537
|
else
|
|
46431
|
-
out.files.push({ path: rel, text });
|
|
46538
|
+
out.files.push({ path: rel, text: text2 });
|
|
46432
46539
|
}
|
|
46433
46540
|
return out;
|
|
46434
46541
|
}
|
|
@@ -46586,14 +46693,14 @@ async function lines(db, u) {
|
|
|
46586
46693
|
return out;
|
|
46587
46694
|
}
|
|
46588
46695
|
function fenced(body) {
|
|
46589
|
-
const
|
|
46696
|
+
const text2 = body.join(`
|
|
46590
46697
|
`);
|
|
46591
46698
|
let longest = 0;
|
|
46592
|
-
for (const m of
|
|
46699
|
+
for (const m of text2.matchAll(/`+/g))
|
|
46593
46700
|
longest = Math.max(longest, m[0].length);
|
|
46594
46701
|
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
46595
46702
|
return `${fence}text
|
|
46596
|
-
${
|
|
46703
|
+
${text2}
|
|
46597
46704
|
${fence}`;
|
|
46598
46705
|
}
|
|
46599
46706
|
async function replaced(db, from) {
|
|
@@ -46809,19 +46916,6 @@ async function fieldsText(db, projectId) {
|
|
|
46809
46916
|
return framed(await fieldsTable(db, projectId));
|
|
46810
46917
|
}
|
|
46811
46918
|
|
|
46812
|
-
// server/src/knowledge.ts
|
|
46813
|
-
var UNIT_KINDS = [
|
|
46814
|
-
"decision",
|
|
46815
|
-
"implementation",
|
|
46816
|
-
"finding",
|
|
46817
|
-
"dead_end",
|
|
46818
|
-
"question",
|
|
46819
|
-
"constraint"
|
|
46820
|
-
];
|
|
46821
|
-
var LIFECYCLES = ["candidate", "active", "superseded", "withdrawn"];
|
|
46822
|
-
var HOSTS = ["claude-code", "codex"];
|
|
46823
|
-
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
46824
|
-
|
|
46825
46919
|
// server/src/overview.ts
|
|
46826
46920
|
import path6 from "node:path";
|
|
46827
46921
|
var OVERVIEW_LIMITS = { records: 50, key: 200, text: 300, paths: 520, heading: 120 };
|
|
@@ -46955,8 +47049,8 @@ async function lookOverview(db, projectId, root) {
|
|
|
46955
47049
|
const scan = ruleFiles(root);
|
|
46956
47050
|
const found = [];
|
|
46957
47051
|
for (const f of scan.files)
|
|
46958
|
-
for (const [i,
|
|
46959
|
-
for (const m of
|
|
47052
|
+
for (const [i, text2] of f.text.split(/\r?\n/).entries())
|
|
47053
|
+
for (const m of text2.matchAll(MARKER))
|
|
46960
47054
|
found.push({ file: f.path, line: i + 1, key: m[1] ?? "" });
|
|
46961
47055
|
const keys = [...new Set(found.map((f) => f.key))];
|
|
46962
47056
|
const units = new Map;
|
|
@@ -47112,7 +47206,7 @@ function gitPath(token, prefix) {
|
|
|
47112
47206
|
}
|
|
47113
47207
|
return prefix && out.startsWith(prefix) ? out.slice(prefix.length) : out;
|
|
47114
47208
|
}
|
|
47115
|
-
function parseDiff(
|
|
47209
|
+
function parseDiff(text2) {
|
|
47116
47210
|
const files = [];
|
|
47117
47211
|
const at = (path8) => {
|
|
47118
47212
|
const found = files.find((f2) => f2.path === path8);
|
|
@@ -47132,7 +47226,7 @@ function parseDiff(text) {
|
|
|
47132
47226
|
at(block);
|
|
47133
47227
|
block = null;
|
|
47134
47228
|
};
|
|
47135
|
-
const rows =
|
|
47229
|
+
const rows = text2.split(/\r?\n/);
|
|
47136
47230
|
for (const [i, raw] of rows.entries()) {
|
|
47137
47231
|
if (cur && (oldLeft > 0 || newLeft > 0)) {
|
|
47138
47232
|
if (raw.startsWith("+")) {
|
|
@@ -47331,7 +47425,7 @@ ${framed(c.work.map((w) => `- ${head(inline(w.title), 200)} (${w.status}): ${hea
|
|
|
47331
47425
|
requireRuntime();
|
|
47332
47426
|
var db = openReader();
|
|
47333
47427
|
var VERSION = versionAt(ROOT);
|
|
47334
|
-
var
|
|
47428
|
+
var text2 = (t, isError = false) => ({
|
|
47335
47429
|
content: [{ type: "text", text: t }],
|
|
47336
47430
|
...isError ? { isError: true } : {}
|
|
47337
47431
|
});
|
|
@@ -47377,46 +47471,46 @@ var CWD = exports_external.string().optional().describe("Which project to use. P
|
|
|
47377
47471
|
server.registerTool("status", {
|
|
47378
47472
|
title: "What Sphica holds for this project",
|
|
47379
47473
|
description: "Current work, and how much of this project's history is captured and extracted: sessions still waiting for trace, quarantined records, " + "and candidates without adoption. Use it to know whether an empty search means nothing was decided or nothing was extracted yet.",
|
|
47380
|
-
inputSchema: { cwd: CWD },
|
|
47474
|
+
inputSchema: exports_external.object({ cwd: CWD }).strict(),
|
|
47381
47475
|
annotations: READ_ONLY
|
|
47382
47476
|
}, async (a) => {
|
|
47383
47477
|
try {
|
|
47384
47478
|
const place = identify(a.cwd ?? process.cwd());
|
|
47385
47479
|
if (!place)
|
|
47386
|
-
return
|
|
47480
|
+
return text2("This directory is not in a registered project (run `sphica init` there).");
|
|
47387
47481
|
const id = await projectId(db, place.key);
|
|
47388
47482
|
const name = head(inline(place.name), 200);
|
|
47389
47483
|
if (id === null)
|
|
47390
|
-
return
|
|
47391
|
-
return
|
|
47484
|
+
return text2(`${name} is not registered with Sphica (run \`sphica init\` there).`);
|
|
47485
|
+
return text2(await status(db, id, name));
|
|
47392
47486
|
} catch (e) {
|
|
47393
|
-
return
|
|
47487
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47394
47488
|
}
|
|
47395
47489
|
});
|
|
47396
47490
|
server.registerTool("search", {
|
|
47397
47491
|
title: "Search past decisions and implementation",
|
|
47398
47492
|
description: "Finds records (decisions, constraints, implementations, findings, dead ends, questions) whose text holds most of the query's words, " + "active ones first. Use short queries of the subject's words (identifiers, option names, the domain terms). sources: true searches the " + "captured conversation and pull request text instead. asked: true finds the owner's earlier messages like the query in other sessions, " + "with the records that quote each and whether a decision was recorded. An empty result also says how many weaker matches were left out.",
|
|
47399
|
-
inputSchema: {
|
|
47493
|
+
inputSchema: exports_external.object({
|
|
47400
47494
|
query: exports_external.string().min(1).max(500).describe("Words for the subject, in Japanese or English"),
|
|
47401
47495
|
cwd: CWD,
|
|
47402
47496
|
kinds: exports_external.array(exports_external.enum(UNIT_KINDS)).optional().describe("Only these kinds"),
|
|
47403
47497
|
lifecycles: exports_external.array(exports_external.enum(LIFECYCLES)).optional().describe("Only these states (default: all, active first)"),
|
|
47404
|
-
path: exports_external.string().max(500).optional().describe("Only records anchored to this repository-relative path"),
|
|
47498
|
+
path: exports_external.string().min(1).max(500).optional().describe("Only records anchored to this repository-relative path"),
|
|
47405
47499
|
sources: exports_external.boolean().optional().describe("Search captured sources instead of records"),
|
|
47406
47500
|
asked: exports_external.boolean().optional().describe("Find the owner's earlier messages like the query, what they led to, and repeats with no recorded decision"),
|
|
47407
47501
|
session: exports_external.string().max(200).optional().describe("With asked: this session's id, so its own messages are left out (in Codex pass CODEX_THREAD_ID from your shell; Codex gives MCP servers none)"),
|
|
47408
47502
|
limit: exports_external.number().int().min(1).max(20).optional()
|
|
47409
|
-
},
|
|
47503
|
+
}).strict(),
|
|
47410
47504
|
annotations: READ_ONLY
|
|
47411
47505
|
}, async (a) => {
|
|
47412
47506
|
try {
|
|
47413
47507
|
const p = await projectOf(a.cwd);
|
|
47414
47508
|
if (typeof p === "string")
|
|
47415
|
-
return
|
|
47509
|
+
return text2(p);
|
|
47416
47510
|
const limit = a.limit ?? 8;
|
|
47417
47511
|
if (a.asked) {
|
|
47418
47512
|
if (a.sources || a.path !== undefined)
|
|
47419
|
-
return
|
|
47513
|
+
return text2("asked cannot be combined with sources or path.");
|
|
47420
47514
|
const external2 = [a.session, process.env.CLAUDE_CODE_SESSION_ID, process.env.CODEX_THREAD_ID].filter((x) => !!x);
|
|
47421
47515
|
const r2 = await askedBefore(db, p.id, {
|
|
47422
47516
|
question: a.query,
|
|
@@ -47427,14 +47521,16 @@ server.registerTool("search", {
|
|
|
47427
47521
|
});
|
|
47428
47522
|
const known = external2.length > 0;
|
|
47429
47523
|
if (!r2.messages.length)
|
|
47430
|
-
return
|
|
47431
|
-
return
|
|
47524
|
+
return text2(`No ${known ? "earlier " : ""}owner message ${r2.stopped ? `among the first ${r2.read} candidates by rank ` : ""}holds most of: ${r2.terms.join(", ") || "(no searchable words)"}. ${r2.weaker} weaker matches left out.${r2.stopped ? " Search with more specific words." : ""}${known ? "" : ` ${UNKNOWN_SESSION}`}`);
|
|
47525
|
+
return text2(framed(askedText(r2, known)));
|
|
47432
47526
|
}
|
|
47433
47527
|
if (a.sources) {
|
|
47528
|
+
if (a.path !== undefined)
|
|
47529
|
+
return text2("sources cannot be combined with path.", true);
|
|
47434
47530
|
const r2 = await searchSources(db, p.id, a.query, limit);
|
|
47435
47531
|
if (!r2.hits.length)
|
|
47436
|
-
return
|
|
47437
|
-
return
|
|
47532
|
+
return text2(`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." : ""}`);
|
|
47533
|
+
return text2(framed(r2.hits.map((h) => `## s${h.id}: ${h.kind} ${h.artifact}, ${h.author}, ${h.created_at}
|
|
47438
47534
|
${head(h.text, 800)}
|
|
47439
47535
|
Matched: ${h.matched.join(", ")}`).join(`
|
|
47440
47536
|
|
|
@@ -47447,107 +47543,109 @@ Matched: ${h.matched.join(", ")}`).join(`
|
|
|
47447
47543
|
path: a.path,
|
|
47448
47544
|
limit
|
|
47449
47545
|
});
|
|
47546
|
+
if (r.refused)
|
|
47547
|
+
return text2(`Nothing was searched: ${r.refused}`, true);
|
|
47450
47548
|
if (!r.hits.length)
|
|
47451
|
-
return
|
|
47452
|
-
return
|
|
47549
|
+
return text2(`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.");
|
|
47550
|
+
return text2(framed(`${r.hits.map(hitText).join(`
|
|
47453
47551
|
|
|
47454
47552
|
`)}${stoppedAfter(r)}
|
|
47455
47553
|
|
|
47456
47554
|
Read a record by its key before relying on it.`));
|
|
47457
47555
|
} catch (e) {
|
|
47458
|
-
return
|
|
47556
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47459
47557
|
}
|
|
47460
47558
|
});
|
|
47461
47559
|
server.registerTool("read", {
|
|
47462
47560
|
title: "Read past records and sources in full",
|
|
47463
47561
|
description: "The full record: its text, options, the exact words cited as evidence and adoption with who said them, links (supersedes, conflicts), " + "state history, and each code location checked in the working tree now. Pass keys or u<id> from search, or s<id> for a source.",
|
|
47464
|
-
inputSchema: {
|
|
47562
|
+
inputSchema: exports_external.object({
|
|
47465
47563
|
refs: exports_external.array(exports_external.string().min(1).max(300)).min(1).max(10).describe("Record keys, u<id>, or s<id> (s<id>@<byte> reads a long source on from that byte)"),
|
|
47466
47564
|
cwd: CWD
|
|
47467
|
-
},
|
|
47565
|
+
}).strict(),
|
|
47468
47566
|
annotations: READ_ONLY
|
|
47469
47567
|
}, async (a) => {
|
|
47470
47568
|
try {
|
|
47471
47569
|
const p = await projectOf(a.cwd);
|
|
47472
47570
|
if (typeof p === "string")
|
|
47473
|
-
return
|
|
47571
|
+
return text2(p);
|
|
47474
47572
|
const parts = [];
|
|
47475
47573
|
for (const ref of a.refs) {
|
|
47476
47574
|
const got = /^s\d/.test(ref) ? await readSource(db, p.id, ref) : await readUnit(db, p.id, ref, p.root);
|
|
47477
47575
|
parts.push(got ?? `${head(inline(ref), 200)}: not found in this project`);
|
|
47478
47576
|
}
|
|
47479
|
-
return
|
|
47577
|
+
return text2(framed(parts.join(`
|
|
47480
47578
|
|
|
47481
47579
|
`)));
|
|
47482
47580
|
} catch (e) {
|
|
47483
|
-
return
|
|
47581
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47484
47582
|
}
|
|
47485
47583
|
});
|
|
47486
47584
|
server.registerTool("export", {
|
|
47487
47585
|
title: "Export chosen decisions to a file",
|
|
47488
47586
|
description: "Only for the export Skill, after the owner chose the decisions and the path. Builds one Markdown document of the chosen active decisions, " + "with their quotes and the older decisions each replaced, and checks that the path stays inside the repository. Returns the document to write, " + "or why nothing can be written; it never writes a file itself.",
|
|
47489
|
-
inputSchema: {
|
|
47587
|
+
inputSchema: exports_external.object({
|
|
47490
47588
|
records: exports_external.array(exports_external.string().min(1).max(300)).min(1).max(EXPORT_LIMITS.records).describe("Keys or u<id> of the active decisions the owner chose"),
|
|
47491
47589
|
path: exports_external.string().min(1).max(500).describe("Where the owner wants the file, relative to the repository root"),
|
|
47492
47590
|
cwd: CWD
|
|
47493
|
-
},
|
|
47591
|
+
}).strict(),
|
|
47494
47592
|
annotations: READ_ONLY
|
|
47495
47593
|
}, async (a) => {
|
|
47496
47594
|
try {
|
|
47497
47595
|
const p = await projectOf(a.cwd);
|
|
47498
47596
|
if (typeof p === "string")
|
|
47499
|
-
return
|
|
47597
|
+
return text2(`Nothing was exported: ${p}`, true);
|
|
47500
47598
|
const where = exportPath(p.root, a.path);
|
|
47501
47599
|
if ("error" in where)
|
|
47502
|
-
return
|
|
47600
|
+
return text2(`Nothing was exported: ${where.error}`, true);
|
|
47503
47601
|
const built = await exportDecisions(db, p.id, p.name, a.records);
|
|
47504
47602
|
if ("error" in built)
|
|
47505
|
-
return
|
|
47506
|
-
return
|
|
47603
|
+
return text2(built.error, true);
|
|
47604
|
+
return text2(exportReply(where, built.document));
|
|
47507
47605
|
} catch (e) {
|
|
47508
|
-
return
|
|
47606
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47509
47607
|
}
|
|
47510
47608
|
});
|
|
47511
47609
|
server.registerTool("fields", {
|
|
47512
47610
|
title: "Fields this project tracks",
|
|
47513
47611
|
description: "Only for the fields Skill. The fields the owner defined for this project's records, as a Markdown table: type, allowed values, " + "the record kinds each applies to, how many records carry a value, and the owner's words that defined it.",
|
|
47514
|
-
inputSchema: { cwd: CWD },
|
|
47612
|
+
inputSchema: exports_external.object({ cwd: CWD }).strict(),
|
|
47515
47613
|
annotations: READ_ONLY
|
|
47516
47614
|
}, async (a) => {
|
|
47517
47615
|
try {
|
|
47518
47616
|
const p = await projectOf(a.cwd);
|
|
47519
47617
|
if (typeof p === "string")
|
|
47520
|
-
return
|
|
47521
|
-
return
|
|
47618
|
+
return text2(p, true);
|
|
47619
|
+
return text2(await fieldsText(db, p.id));
|
|
47522
47620
|
} catch (e) {
|
|
47523
|
-
return
|
|
47621
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47524
47622
|
}
|
|
47525
47623
|
});
|
|
47526
47624
|
var DIFF = exports_external.string().min(1).max(2000000).describe("The change under review as a unified diff (git diff output)");
|
|
47527
|
-
var notChecked = (e) =>
|
|
47625
|
+
var notChecked = (e) => text2(`Decision lane: not checked. Sphica unavailable: ${head(reason(e), 300)}. Report the decision check as not run, not as passed.`, true);
|
|
47528
47626
|
server.registerTool("overview", {
|
|
47529
47627
|
title: "Live decisions, and records that need a look",
|
|
47530
47628
|
description: "On request, not before every change. view live lists every active decision and constraint of the project, grouped by the directory it " + "applies to, a page at a time (pass after from the previous page). view look lists live records whose code file is gone or whose symbol " + "is not found, written conditions for reconsidering an option (for you to judge, never applied), and lines in CLAUDE.md, AGENTS.md, or " + ".claude/rules marked <!-- sphica: key --> whose record was replaced or withdrawn. Read a record by its key before relying on it.",
|
|
47531
|
-
inputSchema: {
|
|
47629
|
+
inputSchema: exports_external.object({
|
|
47532
47630
|
view: exports_external.enum(["live", "look"]).describe("live: every active decision and constraint; look: records that need a look"),
|
|
47533
47631
|
after: exports_external.number().int().min(0).optional().describe("With live: the id the previous page said to continue after"),
|
|
47534
47632
|
cwd: CWD
|
|
47535
|
-
},
|
|
47633
|
+
}).strict(),
|
|
47536
47634
|
annotations: READ_ONLY
|
|
47537
47635
|
}, async (a) => {
|
|
47538
47636
|
try {
|
|
47539
47637
|
const p = await projectOf(a.cwd);
|
|
47540
47638
|
if (typeof p === "string")
|
|
47541
|
-
return
|
|
47542
|
-
return
|
|
47639
|
+
return text2(p);
|
|
47640
|
+
return text2(framed(a.view === "live" ? await liveOverview(db, p.id, a.after ?? null) : await lookOverview(db, p.id, p.root)));
|
|
47543
47641
|
} catch (e) {
|
|
47544
|
-
return
|
|
47642
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47545
47643
|
}
|
|
47546
47644
|
});
|
|
47547
47645
|
server.registerTool("review_select", {
|
|
47548
47646
|
title: "Past decisions a change touches",
|
|
47549
47647
|
description: "For a code review: the active decisions, constraints, and implementation records this diff touches (records anchored to a changed path, " + "and records with no code location that forbid or defer an option an added line names). Judge each against the diff, then check the verdicts with review_check.",
|
|
47550
|
-
inputSchema: { diff: DIFF, cwd: CWD },
|
|
47648
|
+
inputSchema: exports_external.object({ diff: DIFF, cwd: CWD }).strict(),
|
|
47551
47649
|
annotations: READ_ONLY
|
|
47552
47650
|
}, async (a) => {
|
|
47553
47651
|
try {
|
|
@@ -47557,8 +47655,8 @@ server.registerTool("review_select", {
|
|
|
47557
47655
|
const files = parseDiff(a.diff);
|
|
47558
47656
|
const hits = await selectForReview(db, p.id, files);
|
|
47559
47657
|
if (!hits.length)
|
|
47560
|
-
return
|
|
47561
|
-
return
|
|
47658
|
+
return text2(`Decision lane: checked. No active record applies to the ${files.length} changed files.`);
|
|
47659
|
+
return text2(`Decision lane: checked. ${hits.length} records apply; read each before judging it.
|
|
47562
47660
|
${framed(hits.map((u) => `- ${u.key} (${u.kind}${u.stance ? ` ${u.stance}` : ""}): ${head(inline(u.text), 300)} [${u.because}]`).join(`
|
|
47563
47661
|
`))}`);
|
|
47564
47662
|
} catch (e) {
|
|
@@ -47568,11 +47666,11 @@ ${framed(hits.map((u) => `- ${u.key} (${u.kind}${u.stance ? ` ${u.stance}` : ""}
|
|
|
47568
47666
|
server.registerTool("review_check", {
|
|
47569
47667
|
title: "Check decision verdicts",
|
|
47570
47668
|
description: "Checks a reviewer's verdicts on the records review_select returned. Each finding: outcome (violation, complies, unrelated, undetermined), " + "unit (the record key), reason, and for violation or complies, evidence: the changed path and an added line number (the path alone for a deleted or renamed-away file). Every record review_select returned needs one outcome. Returns the problems, or none.",
|
|
47571
|
-
inputSchema: {
|
|
47669
|
+
inputSchema: exports_external.object({
|
|
47572
47670
|
diff: DIFF,
|
|
47573
47671
|
findings: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())).max(50),
|
|
47574
47672
|
cwd: CWD
|
|
47575
|
-
},
|
|
47673
|
+
}).strict(),
|
|
47576
47674
|
annotations: READ_ONLY
|
|
47577
47675
|
}, async (a) => {
|
|
47578
47676
|
try {
|
|
@@ -47580,7 +47678,7 @@ server.registerTool("review_check", {
|
|
|
47580
47678
|
if (typeof p === "string")
|
|
47581
47679
|
return notChecked(new Error(p));
|
|
47582
47680
|
const problems = await checkFindings(db, p.id, parseDiff(a.diff), a.findings);
|
|
47583
|
-
return
|
|
47681
|
+
return text2(problems.length ? `${problems.length} problems:
|
|
47584
47682
|
${problems.map((x) => `- ${x}`).join(`
|
|
47585
47683
|
`)}` : "No problems: every verdict is backed.");
|
|
47586
47684
|
} catch (e) {
|