sphica 0.6.13 → 0.6.15

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.
@@ -45869,7 +45869,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
45869
45869
 
45870
45870
  // server/src/sqlite.ts
45871
45871
  var SCHEMA_GENERATION = 2;
45872
- var SCHEMA_REVISION = 4;
45872
+ var SCHEMA_REVISION = 5;
45873
45873
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
45874
45874
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
45875
45875
  function requireRuntime() {
@@ -46047,24 +46047,92 @@ function captureAuthorizer(own2, action, p1, p2, triggerOrView) {
46047
46047
  return C2.SQLITE_DENY;
46048
46048
  }
46049
46049
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
46050
- var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
46051
- var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
46052
- function ingestAuthorizer(action, p1, p2) {
46050
+ var INGEST_INSERTS = new Set([
46051
+ "project",
46052
+ "ingest_source",
46053
+ "extraction_run",
46054
+ "source_processing",
46055
+ "unit",
46056
+ "unit_option",
46057
+ "unit_evidence",
46058
+ "unit_adoption",
46059
+ "unit_link",
46060
+ "unit_state",
46061
+ "unit_anchor",
46062
+ "unit_alias",
46063
+ "field_def",
46064
+ "unit_field",
46065
+ "work",
46066
+ "artifact_link"
46067
+ ]);
46068
+ var RETRACTION = [
46069
+ "retracted_at",
46070
+ "retraction_reason",
46071
+ "retraction_source_id",
46072
+ "retraction_span_start",
46073
+ "retraction_span_end"
46074
+ ];
46075
+ var INGEST_UPDATES = {
46076
+ extraction_run: new Set(["status", "finished_at"]),
46077
+ unit_anchor: new Set(["retired_at", "replaced_by"]),
46078
+ unit_link: new Set(["resolved_at", "resolution"]),
46079
+ unit_evidence: new Set(RETRACTION),
46080
+ unit_adoption: new Set(RETRACTION),
46081
+ work: new Set(["title", "goal", "current", "next", "status", "branch", "run_id", "updated_at"])
46082
+ };
46083
+ var INGEST_DELETES = new Set(["artifact_link"]);
46084
+ var INGEST_TRIGGER_WRITES = {
46085
+ ingest_source_insert: ["insert source"],
46086
+ source_fts_ai: ["insert source_fts"],
46087
+ source_fts_ad: ["delete source_fts"],
46088
+ unit_state_apply: ["update unit"],
46089
+ unit_state_restore: ["insert unit_state"],
46090
+ ...Object.fromEntries([
46091
+ "evidence_i",
46092
+ "evidence_u",
46093
+ "evidence_d",
46094
+ "adoption_i",
46095
+ "adoption_u",
46096
+ "adoption_d",
46097
+ "link_i",
46098
+ "link_u",
46099
+ "anchor_i",
46100
+ "anchor_u",
46101
+ "alias_i",
46102
+ "field_i",
46103
+ "field_d"
46104
+ ].map((t) => [`unit_rev_${t}`, ["update unit"]])),
46105
+ unit_fts_ai: ["insert unit_fts"],
46106
+ unit_fts_ad: ["delete unit_fts"],
46107
+ ...Object.fromEntries(["option_i", "anchor_i", "anchor_u", "anchor_d", "alias_i", "alias_d", "field_i", "field_d"].map((t) => [
46108
+ `unit_fts_${t}`,
46109
+ ["delete unit_fts", "insert unit_fts"]
46110
+ ]))
46111
+ };
46112
+ var opOf = (action) => action === C2.SQLITE_INSERT ? "insert" : action === C2.SQLITE_UPDATE ? "update" : action === C2.SQLITE_DELETE ? "delete" : undefined;
46113
+ function ingestAuthorizer(action, p1, p2, trigger) {
46053
46114
  if (DDL().has(action))
46054
46115
  return C2.SQLITE_DENY;
46055
- if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
46056
- return C2.SQLITE_DENY;
46057
- if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
46058
- return C2.SQLITE_DENY;
46116
+ const op = opOf(action);
46117
+ if (op) {
46118
+ const table = p1 ?? "";
46119
+ if (SHADOW.test(table))
46120
+ return C2.SQLITE_OK;
46121
+ if (trigger !== null)
46122
+ return INGEST_TRIGGER_WRITES[trigger]?.includes(`${op} ${table}`) ? C2.SQLITE_OK : C2.SQLITE_DENY;
46123
+ const allowed = op === "insert" ? INGEST_INSERTS.has(table) : op === "update" ? INGEST_UPDATES[table]?.has(p2 ?? "") : INGEST_DELETES.has(table);
46124
+ return allowed ? C2.SQLITE_OK : C2.SQLITE_DENY;
46125
+ }
46059
46126
  if (action === C2.SQLITE_PRAGMA)
46060
46127
  return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
46061
- return C2.SQLITE_OK;
46128
+ if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_RECURSIVE || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
46129
+ return C2.SQLITE_OK;
46130
+ return C2.SQLITE_DENY;
46062
46131
  }
46063
46132
  var FORGET_WRITES = {
46064
46133
  [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts", "unit_fts"]),
46065
46134
  [C2.SQLITE_DELETE]: new Set([
46066
46135
  "source",
46067
- "external_reference",
46068
46136
  "unit_evidence",
46069
46137
  "unit_adoption",
46070
46138
  "source_processing",
@@ -46104,7 +46172,7 @@ function connectWriter(role, file2 = dbFile(), create = false) {
46104
46172
  throw e;
46105
46173
  }
46106
46174
  if (role === "ingest")
46107
- raw.setAuthorizer(ingestAuthorizer);
46175
+ raw.setAuthorizer((action, p1, p2, _db, trigger) => ingestAuthorizer(action, p1, p2, trigger));
46108
46176
  else if (role === "forget")
46109
46177
  raw.setAuthorizer(forgetAuthorizer);
46110
46178
  else if (role === "capture") {
@@ -46348,7 +46416,7 @@ async function projectId(db, key) {
46348
46416
  function relativeTo(root, file2, cwd = root) {
46349
46417
  const abs = path4.resolve(cwd, file2);
46350
46418
  const rel = path4.relative(root, abs);
46351
- if (!rel || leaves(rel))
46419
+ if (!rel || leaves(rel) || /\p{Cc}/u.test(rel))
46352
46420
  return null;
46353
46421
  return rel.split(path4.sep).join("/");
46354
46422
  }
@@ -46380,7 +46448,7 @@ var git2 = (root, args) => {
46380
46448
  return null;
46381
46449
  }
46382
46450
  };
46383
- var usable = (p) => p !== "" && !p.includes("\\");
46451
+ var usable = (p) => p !== "" && !p.includes("\\") && !/\p{Cc}/u.test(p);
46384
46452
  function snapshot(root) {
46385
46453
  const out = git2(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
46386
46454
  if (out === null)
@@ -47030,7 +47098,7 @@ var item = (v) => ({
47030
47098
  ...v
47031
47099
  });
47032
47100
  var sha = (s) => s && /^[0-9a-f]{40}$/.test(s) ? s : null;
47033
- var cleanPath = (p) => p && !p.startsWith("/") && !p.includes("\\") && !p.split("/").some((x) => x === "" || x === "." || x === "..") ? p : null;
47101
+ var cleanPath = (p) => p && !p.startsWith("/") && !p.includes("\\") && !/\p{Cc}/u.test(p) && !p.split("/").some((x) => x === "" || x === "." || x === "..") ? p : null;
47034
47102
  async function readPull(get, number5, repo = null) {
47035
47103
  const p = await get(`pulls/${number5}`);
47036
47104
  const artifact = `pr:${number5}`;
@@ -47090,7 +47158,7 @@ async function readPull(get, number5, repo = null) {
47090
47158
  createdAt: c.created_at,
47091
47159
  text: c.body,
47092
47160
  path: cleanPath(c.path),
47093
- lines: end && start ? [start, end] : null,
47161
+ lines: cleanPath(c.path) && end && start ? [start, end] : null,
47094
47162
  hunk: c.diff_hunk ?? null,
47095
47163
  commit: sha(c.commit_id)
47096
47164
  }));
@@ -47173,7 +47241,7 @@ async function storeItems(db, projectId2, items) {
47173
47241
  for (const it of items) {
47174
47242
  const kept = fit(it.text);
47175
47243
  const hash2 = sha256(kept.body);
47176
- const latest = await db.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", it.kind).where("external_id", "=", it.externalId).orderBy("revision", "desc").executeTakeFirst();
47244
+ const latest = await db.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", it.kind).where("external_id", "=", it.externalId).where("session_id", "is", null).orderBy("revision", "desc").executeTakeFirst();
47177
47245
  const forgotten = await db.selectFrom("source_forgotten").select(["revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", it.kind).where("external_id", "=", it.externalId).execute();
47178
47246
  const lastForgotten = Math.max(0, ...forgotten.map((f) => f.revision));
47179
47247
  if (latest && latest.revision > lastForgotten && Buffer.from(latest.content_hash).equals(hash2)) {
@@ -47188,19 +47256,20 @@ async function storeItems(db, projectId2, items) {
47188
47256
  const bound = authorId !== null && owners.has(authorId) && SPOKEN.has(it.kind);
47189
47257
  const kind = bound ? "owner" : it.author?.type === "Bot" ? "bot" : "person";
47190
47258
  const created = iso(it.createdAt);
47191
- const row = await db.insertInto("source").values({
47259
+ const revision = Math.max(latest?.revision ?? 0, lastForgotten) + 1;
47260
+ await db.insertInto("ingest_source").values({
47192
47261
  project_id: projectId2,
47193
47262
  kind: it.kind,
47194
47263
  artifact: it.artifact,
47195
47264
  external_id: it.externalId,
47196
- revision: Math.max(latest?.revision ?? 0, lastForgotten) + 1,
47265
+ revision,
47197
47266
  author_kind: kind,
47198
47267
  author_login: it.author?.login ?? null,
47199
47268
  author_external_id: authorId,
47200
47269
  author_association: it.association,
47201
47270
  parent_external_id: it.parent,
47202
47271
  event_kind: it.event,
47203
- url: it.url,
47272
+ url: it.url && /^https?:\/\//.test(it.url) ? it.url : null,
47204
47273
  created_at: created,
47205
47274
  available_at: latest || lastForgotten ? null : created,
47206
47275
  captured_at: now,
@@ -47215,11 +47284,15 @@ async function storeItems(db, projectId2, items) {
47215
47284
  diff_hunk: it.hunk === null ? null : fit(it.hunk).body,
47216
47285
  commit_sha: it.commit,
47217
47286
  indexed: it.kind === "pr_event" ? 0 : 1
47218
- }).returning("id").executeTakeFirstOrThrow();
47219
- ids.push(row.id);
47287
+ }).execute();
47288
+ ids.push(await itemId(db, projectId2, it.kind, it.externalId, revision));
47220
47289
  }
47221
47290
  return ids;
47222
47291
  }
47292
+ async function itemId(db, projectId2, kind, externalId, revision) {
47293
+ const row = await db.selectFrom("source").select("id").where("project_id", "=", projectId2).where("kind", "=", kind).where("external_id", "=", externalId).where("revision", "=", revision).where("session_id", "is", null).executeTakeFirstOrThrow();
47294
+ return row.id;
47295
+ }
47223
47296
  async function linkIssues(db, projectId2, number5, closes) {
47224
47297
  let stale = db.deleteFrom("artifact_link").where("project_id", "=", projectId2).where("from_artifact", "=", `pr:${number5}`).where("kind", "=", "closes");
47225
47298
  if (closes.length)
@@ -47526,7 +47599,8 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
47526
47599
  const linked = [
47527
47600
  ...new Set(record3.units.flatMap((u) => [...u.supersedes ? [u.supersedes] : [], ...u.conflicts]))
47528
47601
  ];
47529
- const others = new Map((linked.length ? await db.selectFrom("unit").select(["id", "key", "lifecycle"]).where("project_id", "=", target.projectId).where("key", "in", linked).execute() : []).map((u) => [u.key, u]));
47602
+ const others = new Map((linked.length ? await db.selectFrom("unit").select(["id", "key", "kind", "lifecycle"]).where("project_id", "=", target.projectId).where("key", "in", linked).execute() : []).map((u) => [u.key, u]));
47603
+ const holders = new Map((others.size ? await db.selectFrom("unit_link as l").innerJoin("unit as n", "n.id", "l.from_unit").select(["l.to_unit", "n.key", "n.lifecycle"]).where("l.kind", "=", "supersedes").where("l.to_unit", "in", [...others.values()].map((o) => o.id)).where("n.lifecycle", "<>", "withdrawn").where("n.extraction", "=", "supported").where("n.unsourced", "=", 0).execute() : []).map((h) => [h.to_unit, h]));
47530
47604
  const maintainers = new Set(record3.units.some((u) => u.supersedes || u.conflicts.length) ? (await db.selectFrom("source").select("author_login").distinct().where("project_id", "=", target.projectId).where("author_association", "in", [...MAINTAINERS]).execute()).flatMap((r) => r.author_login ? [r.author_login] : []) : []);
47531
47605
  const units = [];
47532
47606
  const claimed = new Set;
@@ -47647,6 +47721,22 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
47647
47721
  problems.push(`${key}: another path-only anchor on ${x.path} already covers it; left out`);
47648
47722
  anchors.splice(anchors.indexOf(x), 1);
47649
47723
  }
47724
+ const places = new Set;
47725
+ for (const x of [...anchors]) {
47726
+ const at = [
47727
+ x.path,
47728
+ x.role,
47729
+ x.commit ?? "",
47730
+ x.symbol ?? "",
47731
+ x.symbol ? "" : x.lines ? place(x) : ""
47732
+ ].join("\x00");
47733
+ if (!places.has(at)) {
47734
+ places.add(at);
47735
+ continue;
47736
+ }
47737
+ problems.push(`${key}: the anchor on ${x.path}${x.symbol ? ` ${x.symbol}` : ""} appears twice; left out`);
47738
+ anchors.splice(anchors.indexOf(x), 1);
47739
+ }
47650
47740
  const aliases = [...new Set(u.aliases.map((a) => a.trim()))];
47651
47741
  const bad = aliases.filter((a) => !a || a.length > 40);
47652
47742
  if (bad.length)
@@ -47658,11 +47748,16 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
47658
47748
  errors3.push(`${key}: supersedes ${u.supersedes}, which is not a record of this project`);
47659
47749
  else if (!["active", "candidate"].includes(old.lifecycle))
47660
47750
  errors3.push(`${key}: ${u.supersedes} is already ${old.lifecycle}`);
47661
- else if (claimed.has(old.id))
47751
+ else if (claimed.has(old.id) && !quarantine.length)
47662
47752
  errors3.push(`${key}: another record in this save already supersedes ${u.supersedes}`);
47663
- else
47753
+ else if (!replaceable(u.kind, old.kind))
47754
+ errors3.push(`${key}: a ${u.kind} cannot supersede ${u.supersedes}, a ${old.kind} (a record supersedes one of its own kind; a decision and a constraint can replace each other)`);
47755
+ else if (holders.has(old.id) && !quarantine.length) {
47756
+ const h = holders.get(old.id);
47757
+ errors3.push(`${key}: ${u.supersedes} already has a successor, ${h?.key} (${h?.lifecycle}); withdraw it first, or supersede it instead`);
47758
+ } else
47664
47759
  supersedes = old.id;
47665
- if (supersedes !== null)
47760
+ if (supersedes !== null && !quarantine.length)
47666
47761
  claimed.add(supersedes);
47667
47762
  }
47668
47763
  const conflicts = u.conflicts.flatMap((k) => {
@@ -47764,6 +47859,7 @@ var contentHash = (u) => sha256(JSON.stringify([
47764
47859
  u.revisit_when ?? null,
47765
47860
  u.options.map((o) => o.reconsider_when === undefined ? [o.text, o.outcome, o.why ?? null] : [o.text, o.outcome, o.why ?? null, o.reconsider_when])
47766
47861
  ]));
47862
+ var replaceable = (successor, old) => successor === old || ["decision", "constraint"].includes(successor) && ["decision", "constraint"].includes(old);
47767
47863
  var ACTIVATION = /needs|cannot become active/;
47768
47864
  async function saveRecord(trx, target, runId, checked, looked) {
47769
47865
  if (checked.errors.length)
@@ -47966,9 +48062,14 @@ ${checked.errors.map((e) => ` ${e}`).join(`
47966
48062
  }
47967
48063
  for (const s of new Set(looked))
47968
48064
  await trx.insertInto("source_processing").values({ source_id: s, run_id: runId, outcome: cited.has(s) ? "units" : "no_unit" }).onConflict((oc) => oc.doNothing()).execute();
47969
- await trx.updateTable("extraction_run").set({ status: "saved", finished_at: now }).where("id", "=", runId).execute();
47970
48065
  return saved;
47971
48066
  }
48067
+ async function finishRun(trx, runId) {
48068
+ await trx.updateTable("extraction_run").set((eb) => ({
48069
+ status: "saved",
48070
+ finished_at: eb.fn("max", [eb.val(iso(Date.now())), eb.ref("started_at")])
48071
+ })).where("id", "=", runId).execute();
48072
+ }
47972
48073
 
47973
48074
  // server/src/glean.ts
47974
48075
  var MAX_FILE = 1024 * 1024;
@@ -48175,7 +48276,7 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
48175
48276
  }
48176
48277
  return { s, at };
48177
48278
  };
48178
- const replaced = new Set;
48279
+ const replaced = new Map;
48179
48280
  const anchored = new Set;
48180
48281
  const places = [];
48181
48282
  for (const [i, op] of parsed.data.ops.entries()) {
@@ -48245,17 +48346,20 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
48245
48346
  if (!from)
48246
48347
  errors3.push(`${what}: the from path is not inside the repository`);
48247
48348
  else {
48248
- let q = db.selectFrom("unit_anchor").select("id").where("unit_id", "=", u.id).where("path", "=", from).where("retired_at", "is", null);
48349
+ let q = db.selectFrom("unit_anchor").select(["id", "path", "symbol", "role", "commit_sha"]).where("unit_id", "=", u.id).where("path", "=", from).where("retired_at", "is", null);
48249
48350
  if (op.from.symbol)
48250
48351
  q = q.where("symbol", "=", op.from.symbol);
48251
48352
  const live = await q.execute();
48252
48353
  const name = `${from}${op.from.symbol ? ` ${op.from.symbol}` : ""}`;
48253
48354
  if (live.length === 1) {
48254
48355
  replaces = live[0]?.id ?? null;
48356
+ const held = live[0];
48357
+ if (held && held.commit_sha === null && held.path === repoPath(op.to.path) && held.symbol === (op.to.symbol ?? null) && held.role === op.to.role)
48358
+ errors3.push(`${what}: the anchor on ${name} is already that place`);
48255
48359
  if (replaces !== null && replaced.has(replaces))
48256
48360
  errors3.push(`${what}: another operation in this batch already replaces ${name}`);
48257
48361
  if (replaces !== null)
48258
- replaced.add(replaces);
48362
+ replaced.set(replaces, i);
48259
48363
  } else if (!live.length)
48260
48364
  errors3.push(`${what}: no live anchor on ${name}`);
48261
48365
  else
@@ -48271,13 +48375,26 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
48271
48375
  if (anchored.has(k))
48272
48376
  errors3.push(`${what}: another operation in this batch already anchors ${name}`);
48273
48377
  anchored.add(k);
48274
- places.push({ what, unit: u.id, rel, symbol: dest.symbol ?? null, role: dest.role, commit, name });
48378
+ places.push({
48379
+ what,
48380
+ unit: u.id,
48381
+ rel,
48382
+ symbol: dest.symbol ?? null,
48383
+ role: dest.role,
48384
+ commit,
48385
+ name,
48386
+ replacing: op.op === "replace_anchor" ? i : null
48387
+ });
48275
48388
  }
48276
48389
  if (op.op === "retract_evidence" || op.op === "retract_adoption" || op.op === "withdraw" || op.op === "resolve_conflict") {
48277
48390
  const got = await span(op.reason_source, op.reason_quote, what);
48278
48391
  if (got && got.s.author_kind !== "owner")
48279
48392
  errors3.push(`${what}: only the owner's words can retract, withdraw, or resolve`);
48280
48393
  }
48394
+ if (op.op === "withdraw") {
48395
+ if (u.lifecycle === "superseded")
48396
+ errors3.push(`${what}: ${op.unit} is superseded, so there is nothing live to withdraw (withdraw the record that replaced it, if that one no longer holds)`);
48397
+ }
48281
48398
  if (op.op === "resolve_conflict") {
48282
48399
  const open2 = await db.selectFrom("unit_link as l").innerJoin("unit as a", "a.id", "l.from_unit").innerJoin("unit as b", "b.id", "l.to_unit").select("l.from_unit").where("l.kind", "=", "conflicts").where("l.resolved_at", "is", null).where((eb) => eb.or([
48283
48400
  eb.and([eb("a.id", "=", u.id), eb("b.key", "=", op.with)]),
@@ -48308,11 +48425,14 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
48308
48425
  ops.push({ input: op, unitId: u.id, lifecycle: u.lifecycle, excerpt, retracts, replaces });
48309
48426
  }
48310
48427
  for (const x of places) {
48311
- let q = db.selectFrom("unit_anchor").select("id").where("unit_id", "=", x.unit).where("path", "=", x.rel).where("role", "=", x.role).where("retired_at", "is", null).where("id", "not in", [...replaced, -1]);
48428
+ const gone = [...replaced].filter(([, by]) => x.replacing === null || by < x.replacing).map(([id]) => id);
48429
+ let q = db.selectFrom("unit_anchor").select("id").where("unit_id", "=", x.unit).where("path", "=", x.rel).where("role", "=", x.role).where("retired_at", "is", null).where("id", "not in", [...gone, -1]);
48312
48430
  q = x.symbol ? q.where("symbol", "=", x.symbol) : q.where("symbol", "is", null);
48313
48431
  q = x.commit ? q.where("commit_sha", "=", x.commit) : q.where("commit_sha", "is", null);
48314
- if (await q.executeTakeFirst())
48315
- errors3.push(`${x.what}: the record already has a live anchor on ${x.name}`);
48432
+ const held = await q.executeTakeFirst();
48433
+ if (!held || x.replacing !== null && replaced.get(held.id) === x.replacing)
48434
+ continue;
48435
+ errors3.push(replaced.has(held.id) ? `${x.what}: a later operation of this batch moves the anchor off ${x.name}; put that replacement before this one, or make the moves in two saves` : `${x.what}: the record already has a live anchor on ${x.name}`);
48316
48436
  }
48317
48437
  return { errors: errors3, problems, units, ops };
48318
48438
  }
@@ -48327,19 +48447,20 @@ async function forgottenExcerpt(db, projectId2, x) {
48327
48447
  async function excerptSource(trx, projectId2, x) {
48328
48448
  const external2 = `file:${x.path}@${x.blob}#L${x.lines[0]}-${x.lines[1]}`;
48329
48449
  const hash2 = sha256(x.text);
48330
- const found = await trx.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", "file_excerpt").where("external_id", "=", external2).orderBy("revision", "desc").executeTakeFirst();
48450
+ const found = await trx.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", "file_excerpt").where("external_id", "=", external2).where("session_id", "is", null).orderBy("revision", "desc").executeTakeFirst();
48331
48451
  const forgotten = await forgottenExcerpt(trx, projectId2, x);
48332
48452
  if (found && found.revision > forgotten.last && found.content_hash.equals(hash2))
48333
48453
  return found.id;
48334
48454
  if (forgotten.same)
48335
48455
  throw new Error(`the owner forgot ${x.path} lines ${x.lines.join("-")}; cite something else`);
48336
48456
  const now = iso(Date.now());
48337
- const row = await trx.insertInto("source").values({
48457
+ const revision2 = Math.max(found?.revision ?? 0, forgotten.last) + 1;
48458
+ await trx.insertInto("ingest_source").values({
48338
48459
  project_id: projectId2,
48339
48460
  kind: "file_excerpt",
48340
48461
  artifact: `file:${x.path}`,
48341
48462
  external_id: external2,
48342
- revision: Math.max(found?.revision ?? 0, forgotten.last) + 1,
48463
+ revision: revision2,
48343
48464
  author_kind: "person",
48344
48465
  created_at: now,
48345
48466
  captured_at: now,
@@ -48354,8 +48475,8 @@ async function excerptSource(trx, projectId2, x) {
48354
48475
  commit_sha: x.commit,
48355
48476
  blob_sha: x.blob,
48356
48477
  indexed: 1
48357
- }).returning("id").executeTakeFirstOrThrow();
48358
- return row.id;
48478
+ }).execute();
48479
+ return itemId(trx, projectId2, "file_excerpt", external2, revision2);
48359
48480
  }
48360
48481
  async function move(trx, unitId, to, reason2, sourceId, runId) {
48361
48482
  const u = await trx.selectFrom("unit").select("lifecycle").where("id", "=", unitId).executeTakeFirstOrThrow();
@@ -48392,7 +48513,11 @@ ${c.errors.map((e) => ` ${e}`).join(`
48392
48513
  return { id: s.id, start: at[0], end: at[1] };
48393
48514
  };
48394
48515
  const touched = new Map;
48395
- for (const p of c.ops) {
48516
+ const ordered = [
48517
+ ...c.ops.filter((p) => p.input.op === "replace_anchor"),
48518
+ ...c.ops.filter((p) => p.input.op !== "replace_anchor")
48519
+ ];
48520
+ for (const p of ordered) {
48396
48521
  const op = p.input;
48397
48522
  touched.set(p.unitId, op.unit);
48398
48523
  if (op.op === "add_evidence") {
@@ -48431,6 +48556,11 @@ ${c.errors.map((e) => ` ${e}`).join(`
48431
48556
  refresh(c.units.facts, rel);
48432
48557
  const symbol2 = to.symbol && !symbolMasked(c.units.facts, rel, to.symbol) ? to.symbol : null;
48433
48558
  const at = symbol2 ? symbolAt(c.units.facts, rel, symbol2) : null;
48559
+ if (op.op === "replace_anchor") {
48560
+ 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();
48561
+ if (held?.kind === "implementation" && held.lifecycle === "active" && held.role === "evidence" && (held.commit_sha !== null || held.edit_observation_id !== null))
48562
+ await move(trx, p.unitId, "candidate", "glean: code anchor replaced, support checked again", null, runId);
48563
+ }
48434
48564
  const added = await trx.insertInto("unit_anchor").values({
48435
48565
  unit_id: p.unitId,
48436
48566
  path: rel,
@@ -48445,11 +48575,6 @@ ${c.errors.map((e) => ` ${e}`).join(`
48445
48575
  }).returning("id").executeTakeFirstOrThrow();
48446
48576
  if (op.op === "replace_anchor")
48447
48577
  await trx.updateTable("unit_anchor").set({ retired_at: now, replaced_by: added.id }).where("id", "=", p.replaces ?? -1).where("retired_at", "is", null).execute();
48448
- if (op.op === "replace_anchor") {
48449
- 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();
48450
- if (held?.kind === "implementation" && held.lifecycle === "active" && held.role === "evidence" && (held.commit_sha !== null || held.edit_observation_id !== null))
48451
- await move(trx, p.unitId, "candidate", "glean: code anchor replaced, support checked again", null, runId);
48452
- }
48453
48578
  changed2.push(`${op.unit}: anchor ${op.op === "anchor" ? "added" : "replaced"}`);
48454
48579
  } else {
48455
48580
  const reason2 = await spanOf(op.reason_source, op.reason_quote);
@@ -48470,9 +48595,22 @@ ${c.errors.map((e) => ` ${e}`).join(`
48470
48595
  continue;
48471
48596
  }
48472
48597
  if (op.op === "withdraw") {
48598
+ touched.delete(p.unitId);
48599
+ const held = await trx.selectFrom("unit").select("lifecycle").where("id", "=", p.unitId).executeTakeFirstOrThrow();
48600
+ if (held.lifecycle === "superseded") {
48601
+ changed2.push(`${op.unit}: superseded by a record of this save, so not withdrawn`);
48602
+ continue;
48603
+ }
48604
+ const replaced = await trx.selectFrom("unit_link as l").innerJoin("unit as o", "o.id", "l.to_unit").select(["o.id", "o.key"]).where("l.from_unit", "=", p.unitId).where("l.kind", "=", "supersedes").where("o.lifecycle", "=", "superseded").execute();
48473
48605
  await move(trx, p.unitId, "withdrawn", `withdrawn: ${head(op.reason_quote, 200)}`, reason2.id, runId);
48474
48606
  changed2.push(`${op.unit}: withdrawn`);
48475
- touched.delete(p.unitId);
48607
+ for (const o of replaced) {
48608
+ const now2 = await trx.selectFrom("unit").select("lifecycle").where("id", "=", o.id).executeTakeFirstOrThrow();
48609
+ if (now2.lifecycle !== "candidate")
48610
+ continue;
48611
+ changed2.push(`${o.key}: no longer superseded`);
48612
+ touched.set(o.id, o.key);
48613
+ }
48476
48614
  continue;
48477
48615
  }
48478
48616
  const current2 = await trx.selectFrom("unit").select("lifecycle").where("id", "=", p.unitId).executeTakeFirstOrThrow();
@@ -48500,7 +48638,6 @@ ${c.errors.map((e) => ` ${e}`).join(`
48500
48638
  changed2.push(`${o.key}: superseded`);
48501
48639
  }
48502
48640
  }
48503
- await trx.updateTable("extraction_run").set({ status: "saved", finished_at: now }).where("id", "=", runId).execute();
48504
48641
  return { units, changed: changed2 };
48505
48642
  }
48506
48643
 
@@ -48868,6 +49005,7 @@ async function saveText(db, id, projectId2, root, record3, probe) {
48868
49005
  ].map((q) => q.source));
48869
49006
  return saveRecord(trx, scope.target, run.id, checked, scope.looked.filter((s) => shown.has(s) || cited.has(s)));
48870
49007
  });
49008
+ await finishRun(trx, run.id);
48871
49009
  return [
48872
49010
  ...saved.active.map((k) => `✓ ${k} active`),
48873
49011
  ...saved.superseded.map((k) => `✓ ${k} superseded`),
@@ -48925,7 +49063,6 @@ async function forgetIn(trx, projectId2, ids, at) {
48925
49063
  sources: rows.map((r) => ({ id: r.id, kind: r.kind, artifact: r.artifact, bytes: r.bytes })),
48926
49064
  already,
48927
49065
  units: [],
48928
- references: 0,
48929
49066
  fields: { definitions: 0, values: 0 }
48930
49067
  };
48931
49068
  if (!rows.length)
@@ -48959,9 +49096,6 @@ async function forgetIn(trx, projectId2, ids, at) {
48959
49096
  content_hash: r.content_hash,
48960
49097
  batch_id: batch
48961
49098
  }))).execute();
48962
- for (const t of ["unit_evidence", "unit_adoption"])
48963
- await trx.deleteFrom(t).where("retraction_source_id", "in", targets).where("retracted_at", "is not", null).where("source_id", "not in", targets).execute();
48964
- outcome.references = Number((await trx.deleteFrom("external_reference").where("owner_source_id", "in", targets).executeTakeFirst()).numDeletedRows);
48965
49099
  await trx.deleteFrom("source").where("id", "in", targets).execute();
48966
49100
  const reason2 = `sources ${targets.map((id) => `s${id}`).join(", ")} forgotten by the owner`;
48967
49101
  for (const u of touched) {
@@ -49045,8 +49179,6 @@ function forgetText(o, file3) {
49045
49179
  const change = u.before === u.after ? `stays ${u.after}` : `${u.before} → ${u.after}`;
49046
49180
  lines.push(`- record ${inline(u.key).slice(0, 120)}: ${change}, loses ${plural2(u.removed, "citation")}`);
49047
49181
  }
49048
- if (o.references)
49049
- lines.push(`- ${plural2(o.references, "unfetched reference")} the forgotten messages gave`);
49050
49182
  if (o.fields.definitions || o.fields.values)
49051
49183
  lines.push(`- ${plural2(o.fields.definitions, "field definition")} and ${plural2(o.fields.values, "field value")} go with them`);
49052
49184
  if (o.units.length || o.fields.values)
package/dist/mcp.js CHANGED
@@ -46030,7 +46030,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
46030
46030
 
46031
46031
  // server/src/sqlite.ts
46032
46032
  var SCHEMA_GENERATION = 2;
46033
- var SCHEMA_REVISION = 4;
46033
+ var SCHEMA_REVISION = 5;
46034
46034
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
46035
46035
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
46036
46036
  function requireRuntime() {
@@ -46354,10 +46354,8 @@ async function describe3(db, u, root, asOf) {
46354
46354
  for (const l of links) {
46355
46355
  if (l.kind === "supersedes")
46356
46356
  out.push(l.from_id === u.id ? `Supersedes ${l.to_key}` : `Superseded by ${l.from_key}`);
46357
- else if (l.kind === "conflicts")
46358
- out.push(`Conflicts with ${l.from_id === u.id ? l.to_key : l.from_key}${l.resolved_at ? " (resolved)" : " (unresolved)"}`);
46359
46357
  else
46360
- out.push(`${l.from_id === u.id ? "Implements" : "Implemented by"} ${l.from_id === u.id ? l.to_key : l.from_key}`);
46358
+ out.push(`Conflicts with ${l.from_id === u.id ? l.to_key : l.from_key}${l.resolved_at ? " (resolved)" : " (unresolved)"}`);
46361
46359
  }
46362
46360
  out.push(`History: ${history.map((s) => `${s.to_state} ${s.at} (${s.reason})`).join("; ")}`);
46363
46361
  return out.join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
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",
@@ -73,7 +73,7 @@ read tools `search` and `read`. Pass the repository root as `cwd` to every tool.
73
73
  | `anchor` / `replace_anchor` | Adds a code location, or replaces one whose code moved (`from` and `to`, citing the owner's words); the old one is kept as history. A replacement carries no commit, so an implementation whose proof was the replaced anchor goes back to candidate: add an `anchor` op with `commit` in the same batch to keep it active. A `symbol` that is a key or a value Sphica masks is refused: anchor a name, or the path alone |
74
74
  | `retract_evidence` / `retract_adoption` | Marks a link mistaken, citing the owner's words (`reason_source`, `reason_quote`). When the record cites the same source more than once, add `quote` to say which one. It is kept as history, and the record is judged again |
75
75
  | `resolve_conflict` | Ends an unresolved conflict between `unit` and `with`, citing the owner's words (`reason_source`, `reason_quote`). Until then neither record is shown on its own |
76
- | `withdraw` | Withdraws a record the owner says no longer holds, citing the owner's words |
76
+ | `withdraw` | Withdraws a record the owner says no longer holds, citing the owner's words. A superseded record is refused: withdraw the record that replaced it |
77
77
 
78
78
  Every op carries the `revision` read printed; a record changed since is refused, so read it again. To correct what a record says, write a new
79
79
  record in `units` (trace's shape, keys saved as `glean:<key>`) with `supersedes` naming the old one (citing the owner's words, a maintainer's, or the owner's session, not other people's text); records are never rewritten.