sphica 0.6.18 → 0.6.21
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/migrations/0008.check.sql +33 -0
- package/db/migrations/0008.sql +53 -0
- package/db/schema.sql +16 -1
- package/dist/capture.js +58 -22
- package/dist/cli.js +1184 -619
- package/dist/deliver.js +56 -22
- package/dist/mcp-record.js +67 -33
- package/dist/mcp.js +694 -584
- 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,347 +45629,6 @@ function parseObject(obj, jsonPath, options) {
|
|
|
45627
45629
|
return target;
|
|
45628
45630
|
}
|
|
45629
45631
|
|
|
45630
|
-
// server/src/search.ts
|
|
45631
|
-
var UNIT_PAGE = 200;
|
|
45632
|
-
var UNIT_SCAN_MAX = 2000;
|
|
45633
|
-
var SOURCE_PAGE = 50;
|
|
45634
|
-
var SOURCE_SCAN_MAX = 600;
|
|
45635
|
-
var SOURCE_SCAN_BYTES = 64 * 1024 * 1024;
|
|
45636
|
-
var LIFE_ORDER = { active: 0, candidate: 1, superseded: 2, withdrawn: 3 };
|
|
45637
|
-
var strong = (matched, of) => matched * 2 > of;
|
|
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);
|
|
45690
|
-
return {
|
|
45691
|
-
hits: hits.slice(0, q.limit).map(({ rank: _rank, ...h }) => h),
|
|
45692
|
-
weaker,
|
|
45693
|
-
terms: wanted,
|
|
45694
|
-
stopped,
|
|
45695
|
-
read
|
|
45696
|
-
};
|
|
45697
|
-
}
|
|
45698
|
-
async function liveSuccessors(db, id) {
|
|
45699
|
-
const seen = new Set([id]);
|
|
45700
|
-
const found = [];
|
|
45701
|
-
const replaced = new Set;
|
|
45702
|
-
for (let frontier = [id];frontier.length; ) {
|
|
45703
|
-
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([
|
|
45704
|
-
"l.to_unit",
|
|
45705
|
-
"n.id",
|
|
45706
|
-
"n.key",
|
|
45707
|
-
"n.kind",
|
|
45708
|
-
"n.stance",
|
|
45709
|
-
"n.lifecycle",
|
|
45710
|
-
"n.text",
|
|
45711
|
-
"n.why",
|
|
45712
|
-
"n.revisit_when"
|
|
45713
|
-
]).execute();
|
|
45714
|
-
frontier = [];
|
|
45715
|
-
for (const { to_unit, ...n } of next) {
|
|
45716
|
-
replaced.add(to_unit);
|
|
45717
|
-
if (seen.has(n.id))
|
|
45718
|
-
continue;
|
|
45719
|
-
seen.add(n.id);
|
|
45720
|
-
found.push(n);
|
|
45721
|
-
frontier.push(n.id);
|
|
45722
|
-
}
|
|
45723
|
-
}
|
|
45724
|
-
return found.filter((n) => !replaced.has(n.id));
|
|
45725
|
-
}
|
|
45726
|
-
async function judgeUnits(db, rows, wanted, hits) {
|
|
45727
|
-
const ids = rows.map((r) => r.id);
|
|
45728
|
-
const [options, anchors, aliases, fields] = ids.length ? await Promise.all([
|
|
45729
|
-
db.selectFrom("unit_option").select(["unit_id", "text", "outcome", "why", "reconsider_when"]).where("unit_id", "in", ids).orderBy("position").execute(),
|
|
45730
|
-
db.selectFrom("unit_anchor").select(["unit_id", "path", "symbol", "role"]).where("unit_id", "in", ids).where("retired_at", "is", null).orderBy("id").execute(),
|
|
45731
|
-
db.selectFrom("unit_alias").select(["unit_id", "terms", "content_hash"]).where("unit_id", "in", ids).orderBy("id", "desc").execute(),
|
|
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
|
-
});
|
|
45771
|
-
}
|
|
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
|
-
}
|
|
45835
|
-
|
|
45836
|
-
// server/src/asked.ts
|
|
45837
|
-
var DECIDED_KINDS = new Set(["decision", "constraint"]);
|
|
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
45632
|
// server/src/kysely-node-sqlite.ts
|
|
45972
45633
|
var plain2 = (row) => {
|
|
45973
45634
|
const out = {};
|
|
@@ -46030,7 +45691,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
46030
45691
|
|
|
46031
45692
|
// server/src/sqlite.ts
|
|
46032
45693
|
var SCHEMA_GENERATION = 2;
|
|
46033
|
-
var SCHEMA_REVISION =
|
|
45694
|
+
var SCHEMA_REVISION = 8;
|
|
46034
45695
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
46035
45696
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
46036
45697
|
function requireRuntime() {
|
|
@@ -46057,176 +45718,636 @@ function prepare(raw, check2, busyMs = 5000) {
|
|
|
46057
45718
|
throw new RevisionMismatch(got ?? 0);
|
|
46058
45719
|
}
|
|
46059
45720
|
|
|
46060
|
-
class RevisionMismatch extends Error {
|
|
46061
|
-
constructor(got) {
|
|
46062
|
-
super(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + (got < SCHEMA_REVISION ? `Run \`npm i -g sphica@${packageVersionAt(ROOT) ?? "latest"}\`, then \`sphica init\` to migrate it (records are kept, and a backup is made first).` : "Update sphica."));
|
|
45721
|
+
class RevisionMismatch extends Error {
|
|
45722
|
+
constructor(got) {
|
|
45723
|
+
super(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + (got < SCHEMA_REVISION ? `Run \`npm i -g sphica@${packageVersionAt(ROOT) ?? "latest"}\`, then \`sphica init\` to migrate it (records are kept, and a backup is made first).` : "Update sphica."));
|
|
45724
|
+
}
|
|
45725
|
+
}
|
|
45726
|
+
function generationOf(raw) {
|
|
45727
|
+
const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
|
|
45728
|
+
if (!has) {
|
|
45729
|
+
const any3 = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'project'").get();
|
|
45730
|
+
return any3 ? 1 : null;
|
|
45731
|
+
}
|
|
45732
|
+
return raw.prepare("select generation from sphica_generation").get()?.generation ?? null;
|
|
45733
|
+
}
|
|
45734
|
+
function checkGeneration(raw) {
|
|
45735
|
+
const got = generationOf(raw);
|
|
45736
|
+
if (got === SCHEMA_GENERATION)
|
|
45737
|
+
return;
|
|
45738
|
+
if (got === null)
|
|
45739
|
+
throw new Error("The database has no sphica schema. Create it with `sphica init`.");
|
|
45740
|
+
throw new Error(got < SCHEMA_GENERATION ? "The database was made by Sphica 0.4 or earlier, and this Sphica cannot read it. Move it aside (it is left unchanged), then run `sphica init`." : "The database was made by a newer Sphica. Update sphica.");
|
|
45741
|
+
}
|
|
45742
|
+
var READER_FUNCTIONS = new Set([
|
|
45743
|
+
"bm25",
|
|
45744
|
+
"coalesce",
|
|
45745
|
+
"count",
|
|
45746
|
+
"instr",
|
|
45747
|
+
"json_array_length",
|
|
45748
|
+
"json_extract",
|
|
45749
|
+
"json_group_array",
|
|
45750
|
+
"json_object",
|
|
45751
|
+
"length",
|
|
45752
|
+
"lower",
|
|
45753
|
+
"match",
|
|
45754
|
+
"max",
|
|
45755
|
+
"min",
|
|
45756
|
+
"substr"
|
|
45757
|
+
]);
|
|
45758
|
+
function connectReader(file2 = dbFile()) {
|
|
45759
|
+
requireRuntime();
|
|
45760
|
+
requireFile(file2);
|
|
45761
|
+
const raw = new DatabaseSync(file2, { readOnly: true });
|
|
45762
|
+
try {
|
|
45763
|
+
prepare(raw, "revision");
|
|
45764
|
+
} catch (e) {
|
|
45765
|
+
raw.close();
|
|
45766
|
+
throw e;
|
|
45767
|
+
}
|
|
45768
|
+
raw.setAuthorizer((action, p1, p2) => {
|
|
45769
|
+
if (action === C.SQLITE_READ || action === C.SQLITE_SELECT || action === C.SQLITE_RECURSIVE)
|
|
45770
|
+
return C.SQLITE_OK;
|
|
45771
|
+
if (action === C.SQLITE_FUNCTION)
|
|
45772
|
+
return READER_FUNCTIONS.has((p2 ?? "").toLowerCase()) ? C.SQLITE_OK : C.SQLITE_DENY;
|
|
45773
|
+
if (action === C.SQLITE_PRAGMA && p1 === "data_version" && p2 === null)
|
|
45774
|
+
return C.SQLITE_OK;
|
|
45775
|
+
return C.SQLITE_DENY;
|
|
45776
|
+
});
|
|
45777
|
+
return raw;
|
|
45778
|
+
}
|
|
45779
|
+
|
|
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);
|
|
46063
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
|
+
};
|
|
46064
46070
|
}
|
|
46065
|
-
function
|
|
46066
|
-
const
|
|
46067
|
-
|
|
46068
|
-
|
|
46069
|
-
|
|
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
|
+
}
|
|
46070
46096
|
}
|
|
46071
|
-
return
|
|
46072
|
-
}
|
|
46073
|
-
function checkGeneration(raw) {
|
|
46074
|
-
const got = generationOf(raw);
|
|
46075
|
-
if (got === SCHEMA_GENERATION)
|
|
46076
|
-
return;
|
|
46077
|
-
if (got === null)
|
|
46078
|
-
throw new Error("The database has no sphica schema. Create it with `sphica init`.");
|
|
46079
|
-
throw new Error(got < SCHEMA_GENERATION ? "The database was made by Sphica 0.4 or earlier, and this Sphica cannot read it. Move it aside (it is left unchanged), then run `sphica init`." : "The database was made by a newer Sphica. Update sphica.");
|
|
46097
|
+
return found.filter((n) => !replaced.has(n.id));
|
|
46080
46098
|
}
|
|
46081
|
-
|
|
46082
|
-
|
|
46083
|
-
|
|
46084
|
-
|
|
46085
|
-
|
|
46086
|
-
|
|
46087
|
-
|
|
46088
|
-
|
|
46089
|
-
|
|
46090
|
-
|
|
46091
|
-
|
|
46092
|
-
|
|
46093
|
-
|
|
46094
|
-
|
|
46095
|
-
|
|
46096
|
-
|
|
46097
|
-
|
|
46098
|
-
|
|
46099
|
-
|
|
46100
|
-
|
|
46101
|
-
|
|
46102
|
-
|
|
46103
|
-
|
|
46104
|
-
|
|
46105
|
-
|
|
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
|
+
});
|
|
46106
46144
|
}
|
|
46107
|
-
|
|
46108
|
-
if (action === C.SQLITE_READ || action === C.SQLITE_SELECT || action === C.SQLITE_RECURSIVE)
|
|
46109
|
-
return C.SQLITE_OK;
|
|
46110
|
-
if (action === C.SQLITE_FUNCTION)
|
|
46111
|
-
return READER_FUNCTIONS.has((p2 ?? "").toLowerCase()) ? C.SQLITE_OK : C.SQLITE_DENY;
|
|
46112
|
-
if (action === C.SQLITE_PRAGMA && p1 === "data_version" && p2 === null)
|
|
46113
|
-
return C.SQLITE_OK;
|
|
46114
|
-
return C.SQLITE_DENY;
|
|
46115
|
-
});
|
|
46116
|
-
return raw;
|
|
46145
|
+
return weaker;
|
|
46117
46146
|
}
|
|
46118
|
-
|
|
46119
|
-
|
|
46120
|
-
|
|
46121
|
-
|
|
46122
|
-
|
|
46123
|
-
|
|
46124
|
-
|
|
46125
|
-
|
|
46126
|
-
|
|
46127
|
-
|
|
46128
|
-
|
|
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;
|
|
@@ -47017,7 +47111,7 @@ import fs6 from "node:fs";
|
|
|
47017
47111
|
import path7 from "node:path";
|
|
47018
47112
|
var localFile = () => path7.join(sphicaHome(), "projects.json");
|
|
47019
47113
|
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
47020
|
-
function
|
|
47114
|
+
function legacyRemote(url2) {
|
|
47021
47115
|
const raw = String(url2 ?? "").trim();
|
|
47022
47116
|
if (!raw)
|
|
47023
47117
|
return null;
|
|
@@ -47034,6 +47128,15 @@ function normalizeRemote(url2) {
|
|
|
47034
47128
|
return null;
|
|
47035
47129
|
}
|
|
47036
47130
|
}
|
|
47131
|
+
var fold = (s) => s.replace(/[A-Z]+/g, (m) => m.toLowerCase());
|
|
47132
|
+
function normalizeKey(key) {
|
|
47133
|
+
if (!key.startsWith("git:"))
|
|
47134
|
+
return key;
|
|
47135
|
+
const rest = key.slice(4);
|
|
47136
|
+
const cut2 = rest.indexOf("/");
|
|
47137
|
+
const host = fold(cut2 < 0 ? rest : rest.slice(0, cut2));
|
|
47138
|
+
return host === "github.com" ? fold(key) : `git:${host}${cut2 < 0 ? "" : rest.slice(cut2)}`;
|
|
47139
|
+
}
|
|
47037
47140
|
var git = (dir, ...args) => {
|
|
47038
47141
|
try {
|
|
47039
47142
|
return execFileSync2("git", ["-C", dir, ...args], {
|
|
@@ -47068,14 +47171,17 @@ function identify(dir) {
|
|
|
47068
47171
|
const given = path7.resolve(dir);
|
|
47069
47172
|
const top = git(given, "rev-parse", "--show-toplevel");
|
|
47070
47173
|
const root = top || given;
|
|
47071
|
-
const
|
|
47072
|
-
if (
|
|
47073
|
-
|
|
47174
|
+
const legacy = top ? legacyRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
47175
|
+
if (legacy) {
|
|
47176
|
+
const key = normalizeKey(`git:${legacy}`);
|
|
47177
|
+
const remote = key.slice(4);
|
|
47178
|
+
return { key, legacyKey: `git:${legacy}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
47179
|
+
}
|
|
47074
47180
|
const map3 = localMap();
|
|
47075
47181
|
for (let d = root;; d = path7.dirname(d)) {
|
|
47076
47182
|
const local = map3[d];
|
|
47077
47183
|
if (local && LOCAL_KEY.test(local))
|
|
47078
|
-
return { key: `local:${local}`, root: d, name: local };
|
|
47184
|
+
return { key: `local:${local}`, legacyKey: `local:${local}`, root: d, name: local };
|
|
47079
47185
|
if (top || path7.dirname(d) === d)
|
|
47080
47186
|
return null;
|
|
47081
47187
|
}
|
|
@@ -47112,7 +47218,7 @@ function gitPath(token, prefix) {
|
|
|
47112
47218
|
}
|
|
47113
47219
|
return prefix && out.startsWith(prefix) ? out.slice(prefix.length) : out;
|
|
47114
47220
|
}
|
|
47115
|
-
function parseDiff(
|
|
47221
|
+
function parseDiff(text2) {
|
|
47116
47222
|
const files = [];
|
|
47117
47223
|
const at = (path8) => {
|
|
47118
47224
|
const found = files.find((f2) => f2.path === path8);
|
|
@@ -47132,7 +47238,7 @@ function parseDiff(text) {
|
|
|
47132
47238
|
at(block);
|
|
47133
47239
|
block = null;
|
|
47134
47240
|
};
|
|
47135
|
-
const rows =
|
|
47241
|
+
const rows = text2.split(/\r?\n/);
|
|
47136
47242
|
for (const [i, raw] of rows.entries()) {
|
|
47137
47243
|
if (cur && (oldLeft > 0 || newLeft > 0)) {
|
|
47138
47244
|
if (raw.startsWith("+")) {
|
|
@@ -47331,7 +47437,7 @@ ${framed(c.work.map((w) => `- ${head(inline(w.title), 200)} (${w.status}): ${hea
|
|
|
47331
47437
|
requireRuntime();
|
|
47332
47438
|
var db = openReader();
|
|
47333
47439
|
var VERSION = versionAt(ROOT);
|
|
47334
|
-
var
|
|
47440
|
+
var text2 = (t, isError = false) => ({
|
|
47335
47441
|
content: [{ type: "text", text: t }],
|
|
47336
47442
|
...isError ? { isError: true } : {}
|
|
47337
47443
|
});
|
|
@@ -47377,46 +47483,46 @@ var CWD = exports_external.string().optional().describe("Which project to use. P
|
|
|
47377
47483
|
server.registerTool("status", {
|
|
47378
47484
|
title: "What Sphica holds for this project",
|
|
47379
47485
|
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 },
|
|
47486
|
+
inputSchema: exports_external.object({ cwd: CWD }).strict(),
|
|
47381
47487
|
annotations: READ_ONLY
|
|
47382
47488
|
}, async (a) => {
|
|
47383
47489
|
try {
|
|
47384
47490
|
const place = identify(a.cwd ?? process.cwd());
|
|
47385
47491
|
if (!place)
|
|
47386
|
-
return
|
|
47492
|
+
return text2("This directory is not in a registered project (run `sphica init` there).");
|
|
47387
47493
|
const id = await projectId(db, place.key);
|
|
47388
47494
|
const name = head(inline(place.name), 200);
|
|
47389
47495
|
if (id === null)
|
|
47390
|
-
return
|
|
47391
|
-
return
|
|
47496
|
+
return text2(`${name} is not registered with Sphica (run \`sphica init\` there).`);
|
|
47497
|
+
return text2(await status(db, id, name));
|
|
47392
47498
|
} catch (e) {
|
|
47393
|
-
return
|
|
47499
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47394
47500
|
}
|
|
47395
47501
|
});
|
|
47396
47502
|
server.registerTool("search", {
|
|
47397
47503
|
title: "Search past decisions and implementation",
|
|
47398
47504
|
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: {
|
|
47505
|
+
inputSchema: exports_external.object({
|
|
47400
47506
|
query: exports_external.string().min(1).max(500).describe("Words for the subject, in Japanese or English"),
|
|
47401
47507
|
cwd: CWD,
|
|
47402
47508
|
kinds: exports_external.array(exports_external.enum(UNIT_KINDS)).optional().describe("Only these kinds"),
|
|
47403
47509
|
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"),
|
|
47510
|
+
path: exports_external.string().min(1).max(500).optional().describe("Only records anchored to this repository-relative path"),
|
|
47405
47511
|
sources: exports_external.boolean().optional().describe("Search captured sources instead of records"),
|
|
47406
47512
|
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
47513
|
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
47514
|
limit: exports_external.number().int().min(1).max(20).optional()
|
|
47409
|
-
},
|
|
47515
|
+
}).strict(),
|
|
47410
47516
|
annotations: READ_ONLY
|
|
47411
47517
|
}, async (a) => {
|
|
47412
47518
|
try {
|
|
47413
47519
|
const p = await projectOf(a.cwd);
|
|
47414
47520
|
if (typeof p === "string")
|
|
47415
|
-
return
|
|
47521
|
+
return text2(p);
|
|
47416
47522
|
const limit = a.limit ?? 8;
|
|
47417
47523
|
if (a.asked) {
|
|
47418
47524
|
if (a.sources || a.path !== undefined)
|
|
47419
|
-
return
|
|
47525
|
+
return text2("asked cannot be combined with sources or path.");
|
|
47420
47526
|
const external2 = [a.session, process.env.CLAUDE_CODE_SESSION_ID, process.env.CODEX_THREAD_ID].filter((x) => !!x);
|
|
47421
47527
|
const r2 = await askedBefore(db, p.id, {
|
|
47422
47528
|
question: a.query,
|
|
@@ -47427,14 +47533,16 @@ server.registerTool("search", {
|
|
|
47427
47533
|
});
|
|
47428
47534
|
const known = external2.length > 0;
|
|
47429
47535
|
if (!r2.messages.length)
|
|
47430
|
-
return
|
|
47431
|
-
return
|
|
47536
|
+
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}`}`);
|
|
47537
|
+
return text2(framed(askedText(r2, known)));
|
|
47432
47538
|
}
|
|
47433
47539
|
if (a.sources) {
|
|
47540
|
+
if (a.path !== undefined)
|
|
47541
|
+
return text2("sources cannot be combined with path.", true);
|
|
47434
47542
|
const r2 = await searchSources(db, p.id, a.query, limit);
|
|
47435
47543
|
if (!r2.hits.length)
|
|
47436
|
-
return
|
|
47437
|
-
return
|
|
47544
|
+
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." : ""}`);
|
|
47545
|
+
return text2(framed(r2.hits.map((h) => `## s${h.id}: ${h.kind} ${h.artifact}, ${h.author}, ${h.created_at}
|
|
47438
47546
|
${head(h.text, 800)}
|
|
47439
47547
|
Matched: ${h.matched.join(", ")}`).join(`
|
|
47440
47548
|
|
|
@@ -47447,107 +47555,109 @@ Matched: ${h.matched.join(", ")}`).join(`
|
|
|
47447
47555
|
path: a.path,
|
|
47448
47556
|
limit
|
|
47449
47557
|
});
|
|
47558
|
+
if (r.refused)
|
|
47559
|
+
return text2(`Nothing was searched: ${r.refused}`, true);
|
|
47450
47560
|
if (!r.hits.length)
|
|
47451
|
-
return
|
|
47452
|
-
return
|
|
47561
|
+
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.");
|
|
47562
|
+
return text2(framed(`${r.hits.map(hitText).join(`
|
|
47453
47563
|
|
|
47454
47564
|
`)}${stoppedAfter(r)}
|
|
47455
47565
|
|
|
47456
47566
|
Read a record by its key before relying on it.`));
|
|
47457
47567
|
} catch (e) {
|
|
47458
|
-
return
|
|
47568
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47459
47569
|
}
|
|
47460
47570
|
});
|
|
47461
47571
|
server.registerTool("read", {
|
|
47462
47572
|
title: "Read past records and sources in full",
|
|
47463
47573
|
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: {
|
|
47574
|
+
inputSchema: exports_external.object({
|
|
47465
47575
|
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
47576
|
cwd: CWD
|
|
47467
|
-
},
|
|
47577
|
+
}).strict(),
|
|
47468
47578
|
annotations: READ_ONLY
|
|
47469
47579
|
}, async (a) => {
|
|
47470
47580
|
try {
|
|
47471
47581
|
const p = await projectOf(a.cwd);
|
|
47472
47582
|
if (typeof p === "string")
|
|
47473
|
-
return
|
|
47583
|
+
return text2(p);
|
|
47474
47584
|
const parts = [];
|
|
47475
47585
|
for (const ref of a.refs) {
|
|
47476
47586
|
const got = /^s\d/.test(ref) ? await readSource(db, p.id, ref) : await readUnit(db, p.id, ref, p.root);
|
|
47477
47587
|
parts.push(got ?? `${head(inline(ref), 200)}: not found in this project`);
|
|
47478
47588
|
}
|
|
47479
|
-
return
|
|
47589
|
+
return text2(framed(parts.join(`
|
|
47480
47590
|
|
|
47481
47591
|
`)));
|
|
47482
47592
|
} catch (e) {
|
|
47483
|
-
return
|
|
47593
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47484
47594
|
}
|
|
47485
47595
|
});
|
|
47486
47596
|
server.registerTool("export", {
|
|
47487
47597
|
title: "Export chosen decisions to a file",
|
|
47488
47598
|
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: {
|
|
47599
|
+
inputSchema: exports_external.object({
|
|
47490
47600
|
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
47601
|
path: exports_external.string().min(1).max(500).describe("Where the owner wants the file, relative to the repository root"),
|
|
47492
47602
|
cwd: CWD
|
|
47493
|
-
},
|
|
47603
|
+
}).strict(),
|
|
47494
47604
|
annotations: READ_ONLY
|
|
47495
47605
|
}, async (a) => {
|
|
47496
47606
|
try {
|
|
47497
47607
|
const p = await projectOf(a.cwd);
|
|
47498
47608
|
if (typeof p === "string")
|
|
47499
|
-
return
|
|
47609
|
+
return text2(`Nothing was exported: ${p}`, true);
|
|
47500
47610
|
const where = exportPath(p.root, a.path);
|
|
47501
47611
|
if ("error" in where)
|
|
47502
|
-
return
|
|
47612
|
+
return text2(`Nothing was exported: ${where.error}`, true);
|
|
47503
47613
|
const built = await exportDecisions(db, p.id, p.name, a.records);
|
|
47504
47614
|
if ("error" in built)
|
|
47505
|
-
return
|
|
47506
|
-
return
|
|
47615
|
+
return text2(built.error, true);
|
|
47616
|
+
return text2(exportReply(where, built.document));
|
|
47507
47617
|
} catch (e) {
|
|
47508
|
-
return
|
|
47618
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47509
47619
|
}
|
|
47510
47620
|
});
|
|
47511
47621
|
server.registerTool("fields", {
|
|
47512
47622
|
title: "Fields this project tracks",
|
|
47513
47623
|
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 },
|
|
47624
|
+
inputSchema: exports_external.object({ cwd: CWD }).strict(),
|
|
47515
47625
|
annotations: READ_ONLY
|
|
47516
47626
|
}, async (a) => {
|
|
47517
47627
|
try {
|
|
47518
47628
|
const p = await projectOf(a.cwd);
|
|
47519
47629
|
if (typeof p === "string")
|
|
47520
|
-
return
|
|
47521
|
-
return
|
|
47630
|
+
return text2(p, true);
|
|
47631
|
+
return text2(await fieldsText(db, p.id));
|
|
47522
47632
|
} catch (e) {
|
|
47523
|
-
return
|
|
47633
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47524
47634
|
}
|
|
47525
47635
|
});
|
|
47526
47636
|
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) =>
|
|
47637
|
+
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
47638
|
server.registerTool("overview", {
|
|
47529
47639
|
title: "Live decisions, and records that need a look",
|
|
47530
47640
|
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: {
|
|
47641
|
+
inputSchema: exports_external.object({
|
|
47532
47642
|
view: exports_external.enum(["live", "look"]).describe("live: every active decision and constraint; look: records that need a look"),
|
|
47533
47643
|
after: exports_external.number().int().min(0).optional().describe("With live: the id the previous page said to continue after"),
|
|
47534
47644
|
cwd: CWD
|
|
47535
|
-
},
|
|
47645
|
+
}).strict(),
|
|
47536
47646
|
annotations: READ_ONLY
|
|
47537
47647
|
}, async (a) => {
|
|
47538
47648
|
try {
|
|
47539
47649
|
const p = await projectOf(a.cwd);
|
|
47540
47650
|
if (typeof p === "string")
|
|
47541
|
-
return
|
|
47542
|
-
return
|
|
47651
|
+
return text2(p);
|
|
47652
|
+
return text2(framed(a.view === "live" ? await liveOverview(db, p.id, a.after ?? null) : await lookOverview(db, p.id, p.root)));
|
|
47543
47653
|
} catch (e) {
|
|
47544
|
-
return
|
|
47654
|
+
return text2(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47545
47655
|
}
|
|
47546
47656
|
});
|
|
47547
47657
|
server.registerTool("review_select", {
|
|
47548
47658
|
title: "Past decisions a change touches",
|
|
47549
47659
|
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 },
|
|
47660
|
+
inputSchema: exports_external.object({ diff: DIFF, cwd: CWD }).strict(),
|
|
47551
47661
|
annotations: READ_ONLY
|
|
47552
47662
|
}, async (a) => {
|
|
47553
47663
|
try {
|
|
@@ -47557,8 +47667,8 @@ server.registerTool("review_select", {
|
|
|
47557
47667
|
const files = parseDiff(a.diff);
|
|
47558
47668
|
const hits = await selectForReview(db, p.id, files);
|
|
47559
47669
|
if (!hits.length)
|
|
47560
|
-
return
|
|
47561
|
-
return
|
|
47670
|
+
return text2(`Decision lane: checked. No active record applies to the ${files.length} changed files.`);
|
|
47671
|
+
return text2(`Decision lane: checked. ${hits.length} records apply; read each before judging it.
|
|
47562
47672
|
${framed(hits.map((u) => `- ${u.key} (${u.kind}${u.stance ? ` ${u.stance}` : ""}): ${head(inline(u.text), 300)} [${u.because}]`).join(`
|
|
47563
47673
|
`))}`);
|
|
47564
47674
|
} catch (e) {
|
|
@@ -47568,11 +47678,11 @@ ${framed(hits.map((u) => `- ${u.key} (${u.kind}${u.stance ? ` ${u.stance}` : ""}
|
|
|
47568
47678
|
server.registerTool("review_check", {
|
|
47569
47679
|
title: "Check decision verdicts",
|
|
47570
47680
|
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: {
|
|
47681
|
+
inputSchema: exports_external.object({
|
|
47572
47682
|
diff: DIFF,
|
|
47573
47683
|
findings: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())).max(50),
|
|
47574
47684
|
cwd: CWD
|
|
47575
|
-
},
|
|
47685
|
+
}).strict(),
|
|
47576
47686
|
annotations: READ_ONLY
|
|
47577
47687
|
}, async (a) => {
|
|
47578
47688
|
try {
|
|
@@ -47580,7 +47690,7 @@ server.registerTool("review_check", {
|
|
|
47580
47690
|
if (typeof p === "string")
|
|
47581
47691
|
return notChecked(new Error(p));
|
|
47582
47692
|
const problems = await checkFindings(db, p.id, parseDiff(a.diff), a.findings);
|
|
47583
|
-
return
|
|
47693
|
+
return text2(problems.length ? `${problems.length} problems:
|
|
47584
47694
|
${problems.map((x) => `- ${x}`).join(`
|
|
47585
47695
|
`)}` : "No problems: every verdict is backed.");
|
|
47586
47696
|
} catch (e) {
|