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.
@@ -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
- const st = fs3.lstatSync(abs, { throwIfNoEntry: false });
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"], { detached: true, stdio: "ignore" }).unref();
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
- if (f.files.get(rel)?.read.hash !== read.hash)
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
- if (a.symbol && !symbolMasked(facts, p, a.symbol) && !a.lines)
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 && a.length <= 40),
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
- ops.push({ input: op, unitId: u.id, lifecycle: u.lifecycle, excerpt, retracts, replaces });
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 held = 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();
48633
- if (held?.kind === "implementation" && held.lifecycle === "active" && held.role === "evidence" && (held.commit_sha !== null || held.edit_observation_id !== null))
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
- const st = fs3.lstatSync(abs, { throwIfNoEntry: false });
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
- async function describe3(db, u, root, asOf) {
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" ? " — needs review: the code it points at is gone" : ""}`);
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 \"${CLAUDE_PLUGIN_ROOT}/dist/capture.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/capture.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/capture.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/capture.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js\"",
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 \"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js\"",
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.24",
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",