sphica 0.6.24 → 0.6.26
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/README.md +1 -2
- package/dist/capture.js +5 -1
- package/dist/cli.js +118 -60
- package/dist/deliver.js +177 -19564
- package/dist/mcp-record.js +210 -13
- package/dist/mcp.js +66 -7
- package/hooks/hooks.json +37 -10
- package/package.json +1 -1
- package/skills/glean/SKILL.md +15 -2
- package/skills/harvest/SKILL.md +3 -1
- package/skills/trace/SKILL.md +2 -1
package/dist/mcp-record.js
CHANGED
|
@@ -46283,7 +46283,12 @@ function readBytes(root, rel) {
|
|
|
46283
46283
|
const abs = path3.join(root, rel);
|
|
46284
46284
|
if (leaves(path3.relative(root, abs)))
|
|
46285
46285
|
return;
|
|
46286
|
-
|
|
46286
|
+
let st;
|
|
46287
|
+
try {
|
|
46288
|
+
st = fs3.lstatSync(abs, { throwIfNoEntry: false });
|
|
46289
|
+
} catch (e) {
|
|
46290
|
+
return e.code === "ENOTDIR" ? null : undefined;
|
|
46291
|
+
}
|
|
46287
46292
|
if (!st)
|
|
46288
46293
|
return null;
|
|
46289
46294
|
if (!st.isFile() || st.size > MAX_BYTES)
|
|
@@ -46359,6 +46364,52 @@ ${l}`).join("");
|
|
|
46359
46364
|
const cut = mask(before + line) !== mask(before) + own2 || mask(line + after) !== own2 + mask(after);
|
|
46360
46365
|
return { line: i + 1, excerpt: cut ? "[redacted]" : own2.trim().slice(0, 200) };
|
|
46361
46366
|
}
|
|
46367
|
+
function fileState(root, rel) {
|
|
46368
|
+
if (!root)
|
|
46369
|
+
return "unknown";
|
|
46370
|
+
if (leaves(path3.relative(root, path3.join(root, rel))))
|
|
46371
|
+
return "unknown";
|
|
46372
|
+
let realRoot;
|
|
46373
|
+
try {
|
|
46374
|
+
realRoot = fs3.realpathSync(root);
|
|
46375
|
+
} catch {
|
|
46376
|
+
return "unknown";
|
|
46377
|
+
}
|
|
46378
|
+
let at = root;
|
|
46379
|
+
for (const part of rel.split("/")) {
|
|
46380
|
+
at = path3.join(at, part);
|
|
46381
|
+
let st;
|
|
46382
|
+
try {
|
|
46383
|
+
st = fs3.lstatSync(at, { throwIfNoEntry: false });
|
|
46384
|
+
} catch (e) {
|
|
46385
|
+
if (e.code === "ENOTDIR")
|
|
46386
|
+
return "gone";
|
|
46387
|
+
return "unknown";
|
|
46388
|
+
}
|
|
46389
|
+
if (!st)
|
|
46390
|
+
return "gone";
|
|
46391
|
+
if (st.isSymbolicLink()) {
|
|
46392
|
+
try {
|
|
46393
|
+
if (leaves(path3.relative(realRoot, fs3.realpathSync(at))))
|
|
46394
|
+
return "unknown";
|
|
46395
|
+
} catch {
|
|
46396
|
+
return "unknown";
|
|
46397
|
+
}
|
|
46398
|
+
}
|
|
46399
|
+
}
|
|
46400
|
+
return "present";
|
|
46401
|
+
}
|
|
46402
|
+
function pathKind(root, rel) {
|
|
46403
|
+
const state = fileState(root, rel);
|
|
46404
|
+
if (state !== "present" || !root)
|
|
46405
|
+
return state === "gone" ? "gone" : "unknown";
|
|
46406
|
+
try {
|
|
46407
|
+
const st = fs3.statSync(path3.join(root, rel));
|
|
46408
|
+
return st.isFile() ? "file" : st.isDirectory() ? "directory" : "unknown";
|
|
46409
|
+
} catch {
|
|
46410
|
+
return "unknown";
|
|
46411
|
+
}
|
|
46412
|
+
}
|
|
46362
46413
|
|
|
46363
46414
|
// server/src/project.ts
|
|
46364
46415
|
var localFile = () => path4.join(sphicaHome(), "projects.json");
|
|
@@ -47109,7 +47160,11 @@ async function main() {
|
|
|
47109
47160
|
if (notice)
|
|
47110
47161
|
process.stdout.write(JSON.stringify({ systemMessage: notice }));
|
|
47111
47162
|
if (send)
|
|
47112
|
-
spawn(process.execPath, [process.argv[1] ?? "", "--flush"], {
|
|
47163
|
+
spawn(process.execPath, [process.argv[1] ?? "", "--flush"], {
|
|
47164
|
+
detached: true,
|
|
47165
|
+
stdio: "ignore",
|
|
47166
|
+
windowsHide: true
|
|
47167
|
+
}).unref();
|
|
47113
47168
|
}
|
|
47114
47169
|
if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
47115
47170
|
main().catch(() => {});
|
|
@@ -47419,19 +47474,31 @@ function commitHolds(root, commit, rel) {
|
|
|
47419
47474
|
return false;
|
|
47420
47475
|
}
|
|
47421
47476
|
}
|
|
47477
|
+
function repoFiles(root) {
|
|
47478
|
+
try {
|
|
47479
|
+
const list = (...args) => cleanGit(root, ["ls-files", "-z", ...args], 32 * 1024 * 1024).toString("utf8").split("\x00").filter(Boolean);
|
|
47480
|
+
const deleted = new Set(list("--deleted"));
|
|
47481
|
+
return [...new Set(list("--cached", "--others", "--exclude-standard"))].filter((p) => !deleted.has(p));
|
|
47482
|
+
} catch {
|
|
47483
|
+
return null;
|
|
47484
|
+
}
|
|
47485
|
+
}
|
|
47422
47486
|
|
|
47423
47487
|
// server/src/repo-facts.ts
|
|
47424
47488
|
var PROBE = {
|
|
47425
47489
|
read: readRepoText,
|
|
47426
47490
|
masks: masksSymbolIn,
|
|
47427
47491
|
locate: locateIn,
|
|
47428
|
-
holds: commitHolds
|
|
47492
|
+
holds: commitHolds,
|
|
47493
|
+
kind: pathKind,
|
|
47494
|
+
files: repoFiles
|
|
47429
47495
|
};
|
|
47430
47496
|
var repoFacts = (root, probe = PROBE) => ({
|
|
47431
47497
|
root,
|
|
47432
47498
|
probe,
|
|
47433
47499
|
files: new Map,
|
|
47434
|
-
commits: new Map
|
|
47500
|
+
commits: new Map,
|
|
47501
|
+
near: new Map
|
|
47435
47502
|
});
|
|
47436
47503
|
function file2(f, rel) {
|
|
47437
47504
|
let got = f.files.get(rel);
|
|
@@ -47458,6 +47525,53 @@ function symbolAt(f, rel, symbol2) {
|
|
|
47458
47525
|
got.at.set(symbol2, f.probe.locate(got.read.text, symbol2));
|
|
47459
47526
|
return got.at.get(symbol2) ?? null;
|
|
47460
47527
|
}
|
|
47528
|
+
function kindOf(f, rel) {
|
|
47529
|
+
const got = file2(f, rel);
|
|
47530
|
+
got.kind ??= f.probe.kind(f.root, rel);
|
|
47531
|
+
return got.kind;
|
|
47532
|
+
}
|
|
47533
|
+
var NEAR_FOR = 5;
|
|
47534
|
+
function listFilesIfGone(f, rel) {
|
|
47535
|
+
if (!f.root || f.near.has(rel) || f.near.size >= NEAR_FOR || kindOf(f, rel) !== "gone")
|
|
47536
|
+
return;
|
|
47537
|
+
if (f.listing === undefined)
|
|
47538
|
+
f.listing = f.probe.files(f.root);
|
|
47539
|
+
f.near.set(rel, f.listing ? near(f.listing, rel) : []);
|
|
47540
|
+
}
|
|
47541
|
+
var nearPaths = (f, rel) => f.near.get(rel);
|
|
47542
|
+
function near(files, rel) {
|
|
47543
|
+
const name = (p) => p.slice(p.lastIndexOf("/") + 1);
|
|
47544
|
+
const own2 = name(rel);
|
|
47545
|
+
const limit = Math.max(1, Math.floor(own2.length / 3));
|
|
47546
|
+
const shared = (p) => {
|
|
47547
|
+
let i = 0;
|
|
47548
|
+
while (i < p.length && p[i] === rel[i])
|
|
47549
|
+
i++;
|
|
47550
|
+
return i;
|
|
47551
|
+
};
|
|
47552
|
+
return files.filter((p) => p !== rel).map((p) => ({ p, d: distance(name(p), own2, limit) })).filter((x) => x.d <= limit).map((x) => ({ ...x, s: shared(x.p), l: Math.abs(x.p.length - rel.length) })).sort((a, b) => a.d - b.d || b.s - a.s || a.l - b.l || a.p.localeCompare(b.p)).slice(0, 3).map((x) => x.p);
|
|
47553
|
+
}
|
|
47554
|
+
function distance(a, b, limit) {
|
|
47555
|
+
if (Math.abs(a.length - b.length) > limit)
|
|
47556
|
+
return limit + 1;
|
|
47557
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
47558
|
+
for (let i = 1;i <= a.length; i++) {
|
|
47559
|
+
const row = [i];
|
|
47560
|
+
let least = i;
|
|
47561
|
+
for (let j = 1;j <= b.length; j++) {
|
|
47562
|
+
const v = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
47563
|
+
row.push(v);
|
|
47564
|
+
least = Math.min(least, v);
|
|
47565
|
+
}
|
|
47566
|
+
if (least > limit)
|
|
47567
|
+
return limit + 1;
|
|
47568
|
+
prev = row;
|
|
47569
|
+
}
|
|
47570
|
+
return prev[b.length] ?? limit + 1;
|
|
47571
|
+
}
|
|
47572
|
+
function symbolMissing(f, rel, symbol2) {
|
|
47573
|
+
return Boolean(f.root) && typeof file2(f, rel).read.text === "string" && symbolAt(f, rel, symbol2) === null;
|
|
47574
|
+
}
|
|
47461
47575
|
function commitHeld(f, commit, rel) {
|
|
47462
47576
|
if (!f.root)
|
|
47463
47577
|
return false;
|
|
@@ -47471,8 +47585,11 @@ function commitHeld(f, commit, rel) {
|
|
|
47471
47585
|
}
|
|
47472
47586
|
function refresh(f, rel) {
|
|
47473
47587
|
const read = f.probe.read(f.root, rel);
|
|
47474
|
-
|
|
47588
|
+
const got = f.files.get(rel);
|
|
47589
|
+
if (got?.read.hash !== read.hash)
|
|
47475
47590
|
f.files.set(rel, { read, masks: new Map, at: new Map });
|
|
47591
|
+
else if (read.hash === "unreadable")
|
|
47592
|
+
got.kind = undefined;
|
|
47476
47593
|
}
|
|
47477
47594
|
|
|
47478
47595
|
// server/src/record.ts
|
|
@@ -47549,6 +47666,25 @@ function locate(body, quote) {
|
|
|
47549
47666
|
const at = Buffer.from(body, "utf8").indexOf(Buffer.from(quote, "utf8"));
|
|
47550
47667
|
return at < 0 ? null : [at, at + Buffer.byteLength(quote, "utf8")];
|
|
47551
47668
|
}
|
|
47669
|
+
function anchorProblem(facts, a) {
|
|
47670
|
+
if (!facts.root || a.held)
|
|
47671
|
+
return null;
|
|
47672
|
+
const kind = kindOf(facts, a.path);
|
|
47673
|
+
const fix = "fix it and check again, or keep it if you know it is right";
|
|
47674
|
+
const at = inline(a.path);
|
|
47675
|
+
if (kind === "gone") {
|
|
47676
|
+
if (a.role === "evidence" && a.observed)
|
|
47677
|
+
return null;
|
|
47678
|
+
const near2 = nearPaths(facts, a.path);
|
|
47679
|
+
const hint = near2?.length ? ` (near: ${near2.map((n) => JSON.stringify(inline(n))).join(", ")})` : near2 === undefined ? " (near paths not checked)" : "";
|
|
47680
|
+
return `anchor path ${at} is not in the working tree${hint}; ${fix}`;
|
|
47681
|
+
}
|
|
47682
|
+
if (kind === "directory")
|
|
47683
|
+
return `anchor path ${at} is a directory; anchor a file`;
|
|
47684
|
+
if (kind === "file" && a.symbol && symbolMissing(facts, a.path, a.symbol))
|
|
47685
|
+
return `symbol ${JSON.stringify(inline(head(a.symbol, 80)))} is not found in ${at}; ${fix}`;
|
|
47686
|
+
return null;
|
|
47687
|
+
}
|
|
47552
47688
|
function repoPath(p) {
|
|
47553
47689
|
const s = p.trim().replace(/^\.\//, "");
|
|
47554
47690
|
if (!s || s.startsWith("/") || s.includes("\\") || /^[A-Za-z]:/.test(s) || /\p{Cc}/u.test(s))
|
|
@@ -47567,7 +47703,8 @@ function prepareRecord(root, raw, probe) {
|
|
|
47567
47703
|
const p = repoPath(a.path);
|
|
47568
47704
|
if (!p)
|
|
47569
47705
|
continue;
|
|
47570
|
-
|
|
47706
|
+
listFilesIfGone(facts, p);
|
|
47707
|
+
if (a.symbol && !symbolMasked(facts, p, a.symbol))
|
|
47571
47708
|
symbolAt(facts, p, a.symbol);
|
|
47572
47709
|
if (a.commit)
|
|
47573
47710
|
commitHeld(facts, a.commit, p);
|
|
@@ -47782,6 +47919,15 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
47782
47919
|
if (a.symbol && !symbol2)
|
|
47783
47920
|
fallbacks.add(planned);
|
|
47784
47921
|
anchors.push(planned);
|
|
47922
|
+
const wrong = anchorProblem(facts, {
|
|
47923
|
+
path: p,
|
|
47924
|
+
symbol: symbol2,
|
|
47925
|
+
role: a.role,
|
|
47926
|
+
held: Boolean(commit),
|
|
47927
|
+
observed: observation !== null
|
|
47928
|
+
});
|
|
47929
|
+
if (wrong)
|
|
47930
|
+
problems.push(`${key}: ${wrong}`);
|
|
47785
47931
|
}
|
|
47786
47932
|
const place = (x) => `${x.path}\x00${x.role}\x00${x.commit}\x00${x.lines ? [x.lines[0], Math.max(...x.lines)] : ""}`;
|
|
47787
47933
|
const covered = new Set(anchors.filter((x) => !x.symbol && !fallbacks.has(x)).map(place));
|
|
@@ -47810,7 +47956,7 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
47810
47956
|
anchors.splice(anchors.indexOf(x), 1);
|
|
47811
47957
|
}
|
|
47812
47958
|
const aliases = [...new Set(u.aliases.map((a) => a.trim()))];
|
|
47813
|
-
const bad = aliases.filter((a) => !a || a.length > 40);
|
|
47959
|
+
const bad = aliases.filter((a) => !a || [...a].length > 40 || inline(a) !== a);
|
|
47814
47960
|
if (bad.length)
|
|
47815
47961
|
problems.push(`${key}: aliases must be 1 to 40 characters; left out ${bad.map((a) => JSON.stringify(a)).join(", ")}`);
|
|
47816
47962
|
let supersedes = null;
|
|
@@ -47883,7 +48029,7 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
47883
48029
|
options,
|
|
47884
48030
|
adoption,
|
|
47885
48031
|
anchors,
|
|
47886
|
-
aliases: aliases.filter((a) => a
|
|
48032
|
+
aliases: aliases.filter((a) => !bad.includes(a)),
|
|
47887
48033
|
supersedes,
|
|
47888
48034
|
conflicts,
|
|
47889
48035
|
fields
|
|
@@ -47939,7 +48085,7 @@ async function saveRecord(trx, target, runId, checked, looked) {
|
|
|
47939
48085
|
${checked.errors.map((e) => ` ${e}`).join(`
|
|
47940
48086
|
`)}`);
|
|
47941
48087
|
const now = iso(Date.now());
|
|
47942
|
-
const saved = { active: [], candidates: [], quarantined: [], superseded: [] };
|
|
48088
|
+
const saved = { active: [], candidates: [], quarantined: [], superseded: [], anchorProblems: [] };
|
|
47943
48089
|
const cited = new Set;
|
|
47944
48090
|
for (const d of checked.fieldDefs) {
|
|
47945
48091
|
cited.add(d.source);
|
|
@@ -48026,6 +48172,15 @@ ${checked.errors.map((e) => ` ${e}`).join(`
|
|
|
48026
48172
|
for (const a of p.anchors) {
|
|
48027
48173
|
refresh(checked.facts, a.path);
|
|
48028
48174
|
const symbol2 = a.symbol && !symbolMasked(checked.facts, a.path, a.symbol) ? a.symbol : undefined;
|
|
48175
|
+
const wrong = anchorProblem(checked.facts, {
|
|
48176
|
+
path: a.path,
|
|
48177
|
+
symbol: symbol2,
|
|
48178
|
+
role: a.role,
|
|
48179
|
+
held: Boolean(a.commit),
|
|
48180
|
+
observed: a.observation !== null
|
|
48181
|
+
});
|
|
48182
|
+
if (wrong)
|
|
48183
|
+
saved.anchorProblems.push(`${p.key}: ${wrong}`);
|
|
48029
48184
|
const at = a.lines ? null : symbol2 ? symbolAt(checked.facts, a.path, symbol2) : null;
|
|
48030
48185
|
const lines = a.lines ?? (at ? [at.line, at.line] : null);
|
|
48031
48186
|
await trx.insertInto("unit_anchor").values({
|
|
@@ -48208,6 +48363,7 @@ var Op = exports_external.discriminatedUnion("op", [
|
|
|
48208
48363
|
reason_quote: quote
|
|
48209
48364
|
}).strict(),
|
|
48210
48365
|
exports_external.object({ op: exports_external.literal("withdraw"), unit, revision, reason_source: SOURCE_REF2, reason_quote: quote }).strict(),
|
|
48366
|
+
exports_external.object({ op: exports_external.literal("replace_aliases"), unit, revision, aliases: exports_external.array(exports_external.string()).max(12) }).strict(),
|
|
48211
48367
|
exports_external.object({
|
|
48212
48368
|
op: exports_external.literal("resolve_conflict"),
|
|
48213
48369
|
unit,
|
|
@@ -48299,6 +48455,8 @@ function prepareGlean(root, raw, probe) {
|
|
|
48299
48455
|
excerptOf(facts, op.file);
|
|
48300
48456
|
const pinned = op.op === "anchor" ? op : op.op === "replace_anchor" ? op.to : null;
|
|
48301
48457
|
const rel = pinned && repoPath(pinned.path);
|
|
48458
|
+
if (rel)
|
|
48459
|
+
listFilesIfGone(facts, rel);
|
|
48302
48460
|
if (pinned?.symbol && rel && !symbolMasked(facts, rel, pinned.symbol))
|
|
48303
48461
|
symbolAt(facts, rel, pinned.symbol);
|
|
48304
48462
|
if (op.op === "anchor" && op.commit && rel)
|
|
@@ -48351,13 +48509,18 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
48351
48509
|
const replaced = new Map;
|
|
48352
48510
|
const anchored = new Set;
|
|
48353
48511
|
const places = [];
|
|
48512
|
+
const unsourcedSaid = new Set;
|
|
48354
48513
|
for (const [i, op] of parsed.data.ops.entries()) {
|
|
48355
48514
|
const what = `ops.${i} ${op.op} ${op.unit}`;
|
|
48356
|
-
const u = await db.selectFrom("unit").select(["id", "kind", "lifecycle", "revision"]).where("project_id", "=", target.projectId).where("key", "=", op.unit).executeTakeFirst();
|
|
48515
|
+
const u = await db.selectFrom("unit").select(["id", "kind", "lifecycle", "revision", "unsourced"]).where("project_id", "=", target.projectId).where("key", "=", op.unit).executeTakeFirst();
|
|
48357
48516
|
if (!u) {
|
|
48358
48517
|
errors3.push(`${what}: not a record of this project`);
|
|
48359
48518
|
continue;
|
|
48360
48519
|
}
|
|
48520
|
+
if ((op.op === "add_evidence" || op.op === "adopt") && u.unsourced && !unsourcedSaid.has(u.id)) {
|
|
48521
|
+
unsourcedSaid.add(u.id);
|
|
48522
|
+
problems.push(`${op.unit} is unsourced and cannot become active; adding evidence or adoption does not clear the flag${u.lifecycle === "candidate" ? ". To use this source, save a successor that supersedes it and cites the source" : ""}`);
|
|
48523
|
+
}
|
|
48361
48524
|
if (u.revision !== op.revision) {
|
|
48362
48525
|
errors3.push(`${what}: changed since you read it (revision ${op.revision}, now ${u.revision}); read it again`);
|
|
48363
48526
|
continue;
|
|
@@ -48447,6 +48610,17 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
48447
48610
|
if (anchored.has(k))
|
|
48448
48611
|
errors3.push(`${what}: another operation in this batch already anchors ${name}`);
|
|
48449
48612
|
anchored.add(k);
|
|
48613
|
+
const held = commit !== null && commitHeld(facts, commit, rel);
|
|
48614
|
+
const observed = dest.role === "evidence" && !held && target.sessionId ? await db.selectFrom("edit_observation").select("id").where("session_id", "=", target.sessionId).where("path", "=", rel).executeTakeFirst() !== undefined : false;
|
|
48615
|
+
const wrong = anchorProblem(facts, {
|
|
48616
|
+
path: rel,
|
|
48617
|
+
symbol: dest.symbol && !symbolMasked(facts, rel, dest.symbol) ? dest.symbol : null,
|
|
48618
|
+
role: dest.role,
|
|
48619
|
+
held,
|
|
48620
|
+
observed
|
|
48621
|
+
});
|
|
48622
|
+
if (wrong)
|
|
48623
|
+
problems.push(`${what}: ${wrong}`);
|
|
48450
48624
|
places.push({
|
|
48451
48625
|
what,
|
|
48452
48626
|
unit: u.id,
|
|
@@ -48494,7 +48668,14 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
48494
48668
|
else
|
|
48495
48669
|
errors3.push(`${what}: no live ${noun} cites ${op.source}`);
|
|
48496
48670
|
}
|
|
48497
|
-
|
|
48671
|
+
let aliases = null;
|
|
48672
|
+
if (op.op === "replace_aliases") {
|
|
48673
|
+
aliases = [...new Set(op.aliases.map((a) => a.trim()))];
|
|
48674
|
+
const bad = aliases.filter((a) => !a || [...a].length > 40 || inline(a) !== a);
|
|
48675
|
+
if (bad.length)
|
|
48676
|
+
errors3.push(`${what}: aliases must be 1 to 40 characters; ${bad.map((a) => JSON.stringify(inline(head(a, 60)))).join(", ")}`);
|
|
48677
|
+
}
|
|
48678
|
+
ops.push({ input: op, unitId: u.id, lifecycle: u.lifecycle, excerpt, retracts, replaces, aliases });
|
|
48498
48679
|
}
|
|
48499
48680
|
for (const x of places) {
|
|
48500
48681
|
const gone = [...replaced].filter(([, by]) => x.replacing === null || by < x.replacing).map(([id]) => id);
|
|
@@ -48627,10 +48808,15 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
48627
48808
|
const rel = repoPath(to.path) ?? to.path;
|
|
48628
48809
|
refresh(c.units.facts, rel);
|
|
48629
48810
|
const symbol2 = to.symbol && !symbolMasked(c.units.facts, rel, to.symbol) ? to.symbol : null;
|
|
48811
|
+
const held = op.op === "anchor" && op.commit !== undefined;
|
|
48812
|
+
const observed = to.role === "evidence" && !held && target.sessionId ? await trx.selectFrom("edit_observation").select("id").where("session_id", "=", target.sessionId).where("path", "=", rel).executeTakeFirst() !== undefined : false;
|
|
48813
|
+
const wrong = anchorProblem(c.units.facts, { path: rel, symbol: symbol2, role: to.role, held, observed });
|
|
48814
|
+
if (wrong)
|
|
48815
|
+
units.anchorProblems.push(`${op.unit}: ${wrong}`);
|
|
48630
48816
|
const at = symbol2 ? symbolAt(c.units.facts, rel, symbol2) : null;
|
|
48631
48817
|
if (op.op === "replace_anchor") {
|
|
48632
|
-
const
|
|
48633
|
-
if (
|
|
48818
|
+
const held2 = await trx.selectFrom("unit_anchor as a").innerJoin("unit as u", "u.id", "a.unit_id").select(["u.kind", "u.lifecycle", "a.role", "a.commit_sha", "a.edit_observation_id"]).where("a.id", "=", p.replaces ?? -1).executeTakeFirst();
|
|
48819
|
+
if (held2?.kind === "implementation" && held2.lifecycle === "active" && held2.role === "evidence" && (held2.commit_sha !== null || held2.edit_observation_id !== null))
|
|
48634
48820
|
await move(trx, p.unitId, "candidate", "glean: code anchor replaced, support checked again", null, runId);
|
|
48635
48821
|
}
|
|
48636
48822
|
const added = await trx.insertInto("unit_anchor").values({
|
|
@@ -48648,6 +48834,16 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
48648
48834
|
if (op.op === "replace_anchor")
|
|
48649
48835
|
await trx.updateTable("unit_anchor").set({ retired_at: now, replaced_by: added.id }).where("id", "=", p.replaces ?? -1).where("retired_at", "is", null).execute();
|
|
48650
48836
|
changed2.push(`${op.unit}: anchor ${op.op === "anchor" ? "added" : "replaced"}`);
|
|
48837
|
+
} else if (op.op === "replace_aliases") {
|
|
48838
|
+
const { content_hash } = await trx.selectFrom("unit").select("content_hash").where("id", "=", p.unitId).executeTakeFirstOrThrow();
|
|
48839
|
+
await trx.insertInto("unit_alias").values({
|
|
48840
|
+
unit_id: p.unitId,
|
|
48841
|
+
terms: JSON.stringify(p.aliases ?? []),
|
|
48842
|
+
content_hash,
|
|
48843
|
+
run_id: runId,
|
|
48844
|
+
added_at: now
|
|
48845
|
+
}).execute();
|
|
48846
|
+
changed2.push(`${op.unit}: aliases ${p.aliases?.length ? "replaced" : "cleared"}`);
|
|
48651
48847
|
} else {
|
|
48652
48848
|
const reason2 = await spanOf(op.reason_source, op.reason_quote);
|
|
48653
48849
|
const retraction = {
|
|
@@ -49083,6 +49279,7 @@ async function saveText(db, id, projectId2, root, record3, probe) {
|
|
|
49083
49279
|
...saved.superseded.map((k) => `✓ ${k} superseded`),
|
|
49084
49280
|
...saved.candidates.map((c) => `△ ${c.key} candidate: ${c.why}`),
|
|
49085
49281
|
...saved.quarantined.map((q) => `△ ${q} quarantined`),
|
|
49282
|
+
...saved.anchorProblems.map((a) => `△ ${a}`),
|
|
49086
49283
|
...lines,
|
|
49087
49284
|
"✓ saved"
|
|
49088
49285
|
].join(`
|
package/dist/mcp.js
CHANGED
|
@@ -45848,7 +45848,12 @@ function readBytes(root, rel) {
|
|
|
45848
45848
|
const abs = path3.join(root, rel);
|
|
45849
45849
|
if (leaves(path3.relative(root, abs)))
|
|
45850
45850
|
return;
|
|
45851
|
-
|
|
45851
|
+
let st;
|
|
45852
|
+
try {
|
|
45853
|
+
st = fs3.lstatSync(abs, { throwIfNoEntry: false });
|
|
45854
|
+
} catch (e) {
|
|
45855
|
+
return e.code === "ENOTDIR" ? null : undefined;
|
|
45856
|
+
}
|
|
45852
45857
|
if (!st)
|
|
45853
45858
|
return null;
|
|
45854
45859
|
if (!st.isFile() || st.size > MAX_BYTES)
|
|
@@ -45937,6 +45942,36 @@ function cleanGit(root, args, max = 1024 * 1024) {
|
|
|
45937
45942
|
timeout: 1e4
|
|
45938
45943
|
});
|
|
45939
45944
|
}
|
|
45945
|
+
function renamesSince(root, commit) {
|
|
45946
|
+
const limit = 1000;
|
|
45947
|
+
try {
|
|
45948
|
+
const out = cleanGit(root, ["diff", "-M", `-l${limit}`, "--name-status", "-z", commit, "--"], 32 * 1024 * 1024);
|
|
45949
|
+
const parts = out.toString("utf8").split("\x00");
|
|
45950
|
+
const renames = new Map;
|
|
45951
|
+
const deleted = [];
|
|
45952
|
+
let added = 0;
|
|
45953
|
+
for (let i = 0;i < parts.length; ) {
|
|
45954
|
+
const status = parts[i] ?? "";
|
|
45955
|
+
if (/^[RC]\d*$/.test(status)) {
|
|
45956
|
+
if (status.startsWith("R"))
|
|
45957
|
+
renames.set(parts[i + 1] ?? "", parts[i + 2] ?? "");
|
|
45958
|
+
i += 3;
|
|
45959
|
+
} else {
|
|
45960
|
+
if (status === "D")
|
|
45961
|
+
deleted.push(parts[i + 1] ?? "");
|
|
45962
|
+
if (status === "A")
|
|
45963
|
+
added++;
|
|
45964
|
+
i += 2;
|
|
45965
|
+
}
|
|
45966
|
+
}
|
|
45967
|
+
if (deleted.length * added > limit * limit)
|
|
45968
|
+
for (const d of deleted)
|
|
45969
|
+
renames.set(d, null);
|
|
45970
|
+
return renames;
|
|
45971
|
+
} catch {
|
|
45972
|
+
return null;
|
|
45973
|
+
}
|
|
45974
|
+
}
|
|
45940
45975
|
|
|
45941
45976
|
// server/src/record.ts
|
|
45942
45977
|
var KEY = /^[a-z0-9][a-z0-9._/-]{0,63}$/;
|
|
@@ -46369,15 +46404,16 @@ import path5 from "node:path";
|
|
|
46369
46404
|
var UNSUPPORTED = "unsupported: its owner quote was retracted or forgotten, so it is not the owner's condition";
|
|
46370
46405
|
var cut = (text2, start, end) => Buffer.from(text2, "utf8").subarray(start, end).toString("utf8");
|
|
46371
46406
|
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})`;
|
|
46372
|
-
async function readUnit(db, projectId, ref, root, asOf) {
|
|
46407
|
+
async function readUnit(db, projectId, ref, root, asOf, renames = new Map) {
|
|
46373
46408
|
const byId = /^u([1-9][0-9]{0,15})$/.exec(ref);
|
|
46374
46409
|
const u = await db.selectFrom("unit").selectAll().where("project_id", "=", projectId).where(byId ? "id" : "key", "=", byId ? Number(byId[1]) : ref).executeTakeFirst();
|
|
46375
46410
|
const bare = u ?? (byId ? undefined : await db.selectFrom("unit").selectAll().where("project_id", "=", projectId).where("key", "in", ["trace:", "harvest:", "glean:"].map((o) => o + ref)).execute().then((rows) => rows.length === 1 ? rows[0] : undefined));
|
|
46376
46411
|
if (!bare || asOf && bare.created_at > asOf)
|
|
46377
46412
|
return null;
|
|
46378
|
-
return describe3(db, bare, root, asOf);
|
|
46413
|
+
return describe3(db, bare, root, asOf, renames);
|
|
46379
46414
|
}
|
|
46380
|
-
|
|
46415
|
+
var RENAME_LOOKUPS = 5;
|
|
46416
|
+
async function describe3(db, u, root, asOf, renames) {
|
|
46381
46417
|
const [options, evidence, adoption, anchors, links, states, fields] = await Promise.all([
|
|
46382
46418
|
db.selectFrom("unit_option").select(["id", "text", "outcome", "why", "reconsider_when"]).where("unit_id", "=", u.id).orderBy("position").execute(),
|
|
46383
46419
|
db.selectFrom("unit_evidence as e").innerJoin("source as s", "s.id", "e.source_id").where("e.unit_id", "=", u.id).where("e.added_at", "<=", asOf ?? "9999").select([
|
|
@@ -46488,8 +46524,8 @@ async function describe3(db, u, root, asOf) {
|
|
|
46488
46524
|
out.push("Code (checked in the working tree now; a located symbol does not prove the record still holds):");
|
|
46489
46525
|
for (const a of live) {
|
|
46490
46526
|
const c = checkAnchor(root, a);
|
|
46491
|
-
const where = `${a.path}${a.symbol ? ` ${a.symbol}` : ""}
|
|
46492
|
-
out.push(` - ${where} (${a.role}${a.commit_sha ? `, commit ${a.commit_sha.slice(0, 12)}` : ""}): ${c.state}${c.line ? ` at line ${c.line}` : ""}${c.state === "missing" ?
|
|
46527
|
+
const where = inline(`${a.path}${a.symbol ? ` ${a.symbol}` : ""}`);
|
|
46528
|
+
out.push(` - ${where} (${a.role}${a.commit_sha ? `, commit ${a.commit_sha.slice(0, 12)}` : ""}): ${c.state}${c.line ? ` at line ${c.line}` : ""}${c.state === "missing" ? ` — needs review: the code it points at is gone${movedTo(root, a, renames)}` : ""}`);
|
|
46493
46529
|
}
|
|
46494
46530
|
}
|
|
46495
46531
|
for (const l of links) {
|
|
@@ -46498,10 +46534,30 @@ async function describe3(db, u, root, asOf) {
|
|
|
46498
46534
|
else
|
|
46499
46535
|
out.push(`Conflicts with ${l.from_id === u.id ? l.to_key : l.from_key}${l.resolved_at ? " (resolved)" : " (unresolved)"}`);
|
|
46500
46536
|
}
|
|
46537
|
+
const aliases = await db.selectFrom("unit_alias").select("terms").where("unit_id", "=", u.id).where("content_hash", "=", u.content_hash).where("added_at", "<=", asOf ?? "9999").orderBy("id", "desc").executeTakeFirst();
|
|
46538
|
+
const terms2 = aliases ? JSON.parse(aliases.terms) : [];
|
|
46539
|
+
if (terms2.length)
|
|
46540
|
+
out.push(`Aliases (search only): ${terms2.map((t) => inline(t)).join(", ")}`);
|
|
46501
46541
|
out.push(`History: ${history.map((s) => `${s.to_state} ${s.at} (${s.reason})`).join("; ")}`);
|
|
46502
46542
|
return out.join(`
|
|
46503
46543
|
`);
|
|
46504
46544
|
}
|
|
46545
|
+
function movedTo(root, a, renames) {
|
|
46546
|
+
if (!root || !a.commit_sha || fileState(root, a.path) !== "gone")
|
|
46547
|
+
return "";
|
|
46548
|
+
if (!renames.has(a.commit_sha)) {
|
|
46549
|
+
if (renames.size >= RENAME_LOOKUPS)
|
|
46550
|
+
return "; rename not checked";
|
|
46551
|
+
renames.set(a.commit_sha, renamesSince(root, a.commit_sha));
|
|
46552
|
+
}
|
|
46553
|
+
const seen = renames.get(a.commit_sha);
|
|
46554
|
+
if (!seen)
|
|
46555
|
+
return "; rename not checked";
|
|
46556
|
+
const to = seen.get(a.path);
|
|
46557
|
+
if (to === null)
|
|
46558
|
+
return "; rename not checked";
|
|
46559
|
+
return to ? `; may have moved to ${JSON.stringify(inline(head(to, 300)))} since ${a.commit_sha.slice(0, 12)}` : "";
|
|
46560
|
+
}
|
|
46505
46561
|
async function readSource(db, projectId, ref) {
|
|
46506
46562
|
const m = /^s([1-9][0-9]{0,15})(?:@(\d{1,9}))?$/.exec(ref);
|
|
46507
46563
|
if (!m)
|
|
@@ -47346,6 +47402,8 @@ async function selectForReview(db, projectId2, files) {
|
|
|
47346
47402
|
}
|
|
47347
47403
|
return [...out.values()];
|
|
47348
47404
|
}
|
|
47405
|
+
|
|
47406
|
+
// server/src/review-findings.ts
|
|
47349
47407
|
var Finding = exports_external.object({
|
|
47350
47408
|
outcome: exports_external.enum(["violation", "complies", "unrelated", "undetermined"]),
|
|
47351
47409
|
unit: exports_external.string().min(1),
|
|
@@ -47602,8 +47660,9 @@ server.registerTool("read", {
|
|
|
47602
47660
|
if (typeof p === "string")
|
|
47603
47661
|
return text2(p);
|
|
47604
47662
|
const parts = [];
|
|
47663
|
+
const renames = new Map;
|
|
47605
47664
|
for (const ref of a.refs) {
|
|
47606
|
-
const got = /^s\d/.test(ref) ? await readSource(db, p.id, ref) : await readUnit(db, p.id, ref, p.root);
|
|
47665
|
+
const got = /^s\d/.test(ref) ? await readSource(db, p.id, ref) : await readUnit(db, p.id, ref, p.root, undefined, renames);
|
|
47607
47666
|
parts.push(got ?? `${head(inline(ref), 200)}: not found in this project`);
|
|
47608
47667
|
}
|
|
47609
47668
|
return text2(framed(parts.join(`
|
package/hooks/hooks.json
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
"hooks": [
|
|
6
6
|
{
|
|
7
7
|
"type": "command",
|
|
8
|
-
"command": "node
|
|
8
|
+
"command": "node",
|
|
9
|
+
"args": [
|
|
10
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/capture.js"
|
|
11
|
+
],
|
|
9
12
|
"timeout": 10
|
|
10
13
|
}
|
|
11
14
|
]
|
|
@@ -14,7 +17,10 @@
|
|
|
14
17
|
"hooks": [
|
|
15
18
|
{
|
|
16
19
|
"type": "command",
|
|
17
|
-
"command": "node
|
|
20
|
+
"command": "node",
|
|
21
|
+
"args": [
|
|
22
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js"
|
|
23
|
+
],
|
|
18
24
|
"timeout": 5
|
|
19
25
|
}
|
|
20
26
|
]
|
|
@@ -25,7 +31,10 @@
|
|
|
25
31
|
"hooks": [
|
|
26
32
|
{
|
|
27
33
|
"type": "command",
|
|
28
|
-
"command": "node
|
|
34
|
+
"command": "node",
|
|
35
|
+
"args": [
|
|
36
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js"
|
|
37
|
+
],
|
|
29
38
|
"timeout": 5
|
|
30
39
|
}
|
|
31
40
|
]
|
|
@@ -36,7 +45,10 @@
|
|
|
36
45
|
"hooks": [
|
|
37
46
|
{
|
|
38
47
|
"type": "command",
|
|
39
|
-
"command": "node
|
|
48
|
+
"command": "node",
|
|
49
|
+
"args": [
|
|
50
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/capture.js"
|
|
51
|
+
],
|
|
40
52
|
"async": true
|
|
41
53
|
}
|
|
42
54
|
]
|
|
@@ -45,7 +57,10 @@
|
|
|
45
57
|
"hooks": [
|
|
46
58
|
{
|
|
47
59
|
"type": "command",
|
|
48
|
-
"command": "node
|
|
60
|
+
"command": "node",
|
|
61
|
+
"args": [
|
|
62
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js"
|
|
63
|
+
],
|
|
49
64
|
"timeout": 5
|
|
50
65
|
}
|
|
51
66
|
]
|
|
@@ -57,7 +72,10 @@
|
|
|
57
72
|
"hooks": [
|
|
58
73
|
{
|
|
59
74
|
"type": "command",
|
|
60
|
-
"command": "node
|
|
75
|
+
"command": "node",
|
|
76
|
+
"args": [
|
|
77
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/capture.js"
|
|
78
|
+
],
|
|
61
79
|
"async": true
|
|
62
80
|
}
|
|
63
81
|
]
|
|
@@ -68,7 +86,10 @@
|
|
|
68
86
|
"hooks": [
|
|
69
87
|
{
|
|
70
88
|
"type": "command",
|
|
71
|
-
"command": "node
|
|
89
|
+
"command": "node",
|
|
90
|
+
"args": [
|
|
91
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/capture.js"
|
|
92
|
+
],
|
|
72
93
|
"async": true
|
|
73
94
|
}
|
|
74
95
|
]
|
|
@@ -76,11 +97,14 @@
|
|
|
76
97
|
],
|
|
77
98
|
"PreToolUse": [
|
|
78
99
|
{
|
|
79
|
-
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Read|Skill|Bash",
|
|
100
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Read|Skill|Bash|PowerShell",
|
|
80
101
|
"hooks": [
|
|
81
102
|
{
|
|
82
103
|
"type": "command",
|
|
83
|
-
"command": "node
|
|
104
|
+
"command": "node",
|
|
105
|
+
"args": [
|
|
106
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js"
|
|
107
|
+
],
|
|
84
108
|
"timeout": 5
|
|
85
109
|
}
|
|
86
110
|
]
|
|
@@ -91,7 +115,10 @@
|
|
|
91
115
|
"hooks": [
|
|
92
116
|
{
|
|
93
117
|
"type": "command",
|
|
94
|
-
"command": "node
|
|
118
|
+
"command": "node",
|
|
119
|
+
"args": [
|
|
120
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js"
|
|
121
|
+
],
|
|
95
122
|
"timeout": 5
|
|
96
123
|
}
|
|
97
124
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphica",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.26",
|
|
4
4
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|