sphica 0.6.28 → 0.6.30

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/dist/cli.js CHANGED
@@ -3697,7 +3697,7 @@ ${indent}${caution(note)}` : ""}`);
3697
3697
 
3698
3698
  // server/src/sqlite.ts
3699
3699
  var SCHEMA_GENERATION = 2;
3700
- var SCHEMA_REVISION = 9;
3700
+ var SCHEMA_REVISION = 10;
3701
3701
  var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os2.homedir(), ".sphica");
3702
3702
  var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
3703
3703
  function requireRuntime() {
@@ -14681,6 +14681,7 @@ var CAPTURE_VIEWS = new Set([
14681
14681
  "capture_session",
14682
14682
  "capture_message",
14683
14683
  "capture_edit",
14684
+ "capture_tool_call",
14684
14685
  "capture_delivery",
14685
14686
  "capture_delivery_scoped",
14686
14687
  "capture_delivery_prune"
@@ -14689,6 +14690,7 @@ var TRIGGER_WRITES = {
14689
14690
  capture_session_insert: new Set(["session"]),
14690
14691
  capture_message_insert: new Set(["source"]),
14691
14692
  capture_edit_insert: new Set(["edit_observation"]),
14693
+ capture_tool_call_insert: new Set(["tool_call_observation"]),
14692
14694
  capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
14693
14695
  capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
14694
14696
  source_fts_ai: new Set(["source_fts"])
@@ -14736,6 +14738,7 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
14736
14738
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
14737
14739
  var INGEST_INSERTS = new Set([
14738
14740
  "project",
14741
+ "record_call",
14739
14742
  "ingest_source",
14740
14743
  "extraction_run",
14741
14744
  "source_processing",
@@ -14745,6 +14748,7 @@ var INGEST_INSERTS = new Set([
14745
14748
  "unit_adoption",
14746
14749
  "unit_link",
14747
14750
  "unit_state",
14751
+ "unit_replacement",
14748
14752
  "unit_anchor",
14749
14753
  "unit_alias",
14750
14754
  "field_def",
@@ -14765,6 +14769,7 @@ var INGEST_UPDATES = {
14765
14769
  unit_link: new Set(["resolved_at", "resolution"]),
14766
14770
  unit_evidence: new Set(RETRACTION),
14767
14771
  unit_adoption: new Set(RETRACTION),
14772
+ unit_replacement: new Set(["ended_at", "end_reason", "end_run_id"]),
14768
14773
  work: new Set(["title", "goal", "current", "next", "status", "branch", "run_id", "updated_at"])
14769
14774
  };
14770
14775
  var INGEST_DELETES = new Set(["artifact_link"]);
@@ -14773,7 +14778,6 @@ var INGEST_TRIGGER_WRITES = {
14773
14778
  source_fts_ai: ["insert source_fts"],
14774
14779
  source_fts_ad: ["delete source_fts"],
14775
14780
  unit_state_apply: ["update unit"],
14776
- unit_state_restore: ["insert unit_state"],
14777
14781
  delivery_ad: ["delete delivery_unit"],
14778
14782
  ...Object.fromEntries([
14779
14783
  "evidence_i",
@@ -14784,6 +14788,8 @@ var INGEST_TRIGGER_WRITES = {
14784
14788
  "adoption_d",
14785
14789
  "link_i",
14786
14790
  "link_u",
14791
+ "replacement_i",
14792
+ "replacement_u",
14787
14793
  "anchor_i",
14788
14794
  "anchor_u",
14789
14795
  "alias_i",
@@ -14818,7 +14824,14 @@ function ingestAuthorizer(action, p1, p2, trigger) {
14818
14824
  return C2.SQLITE_DENY;
14819
14825
  }
14820
14826
  var FORGET_WRITES = {
14821
- [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts", "unit_fts"]),
14827
+ [C2.SQLITE_INSERT]: new Set([
14828
+ "forget_batch",
14829
+ "source_forgotten",
14830
+ "unit_state",
14831
+ "unit_replacement",
14832
+ "source_fts",
14833
+ "unit_fts"
14834
+ ]),
14822
14835
  [C2.SQLITE_DELETE]: new Set([
14823
14836
  "source",
14824
14837
  "unit_evidence",
@@ -14832,7 +14845,8 @@ var FORGET_WRITES = {
14832
14845
  };
14833
14846
  var FORGET_UPDATES = {
14834
14847
  unit: new Set(["lifecycle", "revision"]),
14835
- unit_state: new Set(["source_id"])
14848
+ unit_state: new Set(["source_id"]),
14849
+ unit_replacement: new Set(["ended_at", "end_reason", "end_forget_id"])
14836
14850
  };
14837
14851
  function forgetAuthorizer(action, p1, p2) {
14838
14852
  if (SHADOW.test(p1 ?? "") && writes(action))
@@ -15727,6 +15741,30 @@ async function readInput(stream) {
15727
15741
  raw += chunk;
15728
15742
  return JSON.parse(raw || "{}");
15729
15743
  }
15744
+ var RECORD_TOOLS = "mcp__plugin_sphica_record__";
15745
+ var OBSERVE_WAIT_MS = 5000;
15746
+ var hostId = (v) => v && /^[\x21-\x7e]{1,200}$/.test(v) ? v : null;
15747
+ async function observeRecordCall(file, input, owner) {
15748
+ const session = hostId(input.session_id);
15749
+ const toolUse = hostId(input.tool_use_id);
15750
+ const tool = hostId(input.tool_name);
15751
+ if (!session || !toolUse || !tool?.startsWith(RECORD_TOOLS))
15752
+ return;
15753
+ const cap = openWriter("capture", file, OBSERVE_WAIT_MS);
15754
+ try {
15755
+ await cap.insertInto("capture_tool_call").values({
15756
+ host: "claude-code",
15757
+ session_external: session,
15758
+ turn_id: hostId(input.prompt_id),
15759
+ tool_use_id: toolUse,
15760
+ tool_name: tool,
15761
+ owner_turn: owner ? 1 : 0,
15762
+ observed_at: iso(Date.now())
15763
+ }).execute();
15764
+ } finally {
15765
+ await cap.destroy().catch(() => {});
15766
+ }
15767
+ }
15730
15768
  async function main() {
15731
15769
  if (process.argv[2] === "--flush") {
15732
15770
  await flush();
@@ -15734,6 +15772,10 @@ async function main() {
15734
15772
  }
15735
15773
  const input = await readInput(process.stdin);
15736
15774
  const host = process.argv[2] === "codex" ? "codex" : "claude-code";
15775
+ if (host === "claude-code" && input.hook_event_name === "PreToolUse") {
15776
+ await observeRecordCall(dbFile(), input, isOwnerTurn(input));
15777
+ return;
15778
+ }
15737
15779
  if (host === "codex" && input.hook_event_name === "Stop")
15738
15780
  process.stdout.write("{}");
15739
15781
  const { flush: send, notice } = onHook(host, input);
@@ -15862,6 +15904,226 @@ function steps(heading, items, note) {
15862
15904
  return capture((output) => box(body, oneLine(heading), { output, rounded: true, width: "auto" }));
15863
15905
  }
15864
15906
 
15907
+ // server/src/judge.ts
15908
+ var replaceable = (successor, old) => successor === old || ["decision", "constraint"].includes(successor) && ["decision", "constraint"].includes(old);
15909
+ var owned = (kind) => kind === "decision" || kind === "constraint";
15910
+ function judge(s) {
15911
+ const units = new Map(s.units.map((u3) => [u3.id, u3]));
15912
+ const intentOf = new Map(s.intents.map((i2) => [i2.from, i2]));
15913
+ const openInto = new Map(s.open.map((r2) => [r2.to, r2.from]));
15914
+ const waits = new Map;
15915
+ const unfit = (u3) => !u3.sound ? { why: "unsound" } : !u3.supported ? { why: "unsupported" } : owned(u3.kind) && intentOf.has(u3.id) && !u3.ownerAdopted ? { why: "no owner adoption" } : null;
15916
+ const contenders = new Map;
15917
+ for (const i2 of [...intentOf.values()].sort(order)) {
15918
+ const from = units.get(i2.from);
15919
+ const to = units.get(i2.to);
15920
+ if (!from || !to || from.withdrawn)
15921
+ continue;
15922
+ const why = unfit(from) ?? (!replaceable(from.kind, to.kind) ? { why: "kinds" } : to.withdrawn ? { why: "target withdrawn" } : to.quarantined ? { why: "target quarantined" } : null);
15923
+ if (why) {
15924
+ waits.set(from.id, why);
15925
+ continue;
15926
+ }
15927
+ contenders.set(i2.to, [...contenders.get(i2.to) ?? [], i2]);
15928
+ }
15929
+ const holders = new Map;
15930
+ for (const [to, list] of contenders) {
15931
+ const kept = openInto.get(to);
15932
+ const holder = list.some((i2) => i2.from === kept) ? kept : list[0].from;
15933
+ holders.set(to, holder);
15934
+ for (const i2 of list)
15935
+ if (i2.from !== holder)
15936
+ waits.set(i2.from, { why: "place held", holder });
15937
+ }
15938
+ const holds = new Set(holders.values());
15939
+ const lifecycle = new Map;
15940
+ for (const u3 of s.units) {
15941
+ if (u3.withdrawn) {
15942
+ lifecycle.set(u3.id, "withdrawn");
15943
+ waits.delete(u3.id);
15944
+ } else if (holders.has(u3.id))
15945
+ lifecycle.set(u3.id, "superseded");
15946
+ else if (!unfit(u3) && (!intentOf.has(u3.id) || holds.has(u3.id))) {
15947
+ lifecycle.set(u3.id, "active");
15948
+ waits.delete(u3.id);
15949
+ } else {
15950
+ lifecycle.set(u3.id, "candidate");
15951
+ if (!waits.has(u3.id))
15952
+ waits.set(u3.id, unfit(u3) ?? { why: "unsupported" });
15953
+ }
15954
+ }
15955
+ const unstood = new Map;
15956
+ for (const [id, w] of waits)
15957
+ unstood.set(id, w);
15958
+ for (const [id, l] of lifecycle)
15959
+ if (l !== "candidate")
15960
+ waits.delete(id);
15961
+ return { lifecycle, holders, waits, unstood };
15962
+ }
15963
+ function order(a2, b) {
15964
+ return a2.addedAt < b.addedAt ? -1 : a2.addedAt > b.addedAt ? 1 : a2.from - b.from;
15965
+ }
15966
+ function difference(s, j) {
15967
+ const close = [];
15968
+ const open = [];
15969
+ const stored = new Map(s.open.map((r2) => [r2.to, r2.from]));
15970
+ for (const r2 of s.open)
15971
+ if (j.holders.get(r2.to) !== r2.from)
15972
+ close.push(r2);
15973
+ for (const [to, from] of j.holders)
15974
+ if (stored.get(to) !== from)
15975
+ open.push({ from, to });
15976
+ const transitions = [];
15977
+ for (const u3 of s.units) {
15978
+ const to = j.lifecycle.get(u3.id);
15979
+ if (u3.lifecycle === null) {
15980
+ transitions.push({ unit: u3.id, from: null, to: "candidate" });
15981
+ if (to !== "candidate")
15982
+ transitions.push({ unit: u3.id, from: "candidate", to });
15983
+ continue;
15984
+ }
15985
+ if (u3.lifecycle === to)
15986
+ continue;
15987
+ if (u3.lifecycle === "withdrawn")
15988
+ throw new Error(`unit ${u3.id} is withdrawn, which is final`);
15989
+ if (u3.lifecycle === "superseded" && to === "withdrawn")
15990
+ throw new Error(`unit ${u3.id} is superseded; withdraw what replaced it instead`);
15991
+ transitions.push({ unit: u3.id, from: u3.lifecycle, to });
15992
+ }
15993
+ return { close, open, transitions };
15994
+ }
15995
+
15996
+ // server/src/reconcile.ts
15997
+ function snapshotOf(rows, withdraw) {
15998
+ const keys = new Map(rows.units.map((u3) => [u3.id, u3.key]));
15999
+ const missing = new Map;
16000
+ const snapshot2 = {
16001
+ units: rows.units.map((u3) => {
16002
+ const lifecycle = Number(u3.stated) === 1 ? u3.lifecycle : null;
16003
+ const unquoted = Number(u3.unquoted) === 1 && Number(u3.ever) === 0;
16004
+ if (u3.missing)
16005
+ missing.set(u3.id, u3.missing);
16006
+ else if (unquoted)
16007
+ missing.set(u3.id, "a reconsider condition needs a quote of the owner");
16008
+ return {
16009
+ id: u3.id,
16010
+ kind: u3.kind,
16011
+ lifecycle,
16012
+ withdrawn: lifecycle === "withdrawn" || withdraw.has(u3.id),
16013
+ sound: u3.extraction === "supported" && Number(u3.unsourced) === 0,
16014
+ quarantined: u3.extraction !== "supported",
16015
+ supported: u3.missing === null && !unquoted,
16016
+ ownerAdopted: Number(u3.owned) === 1
16017
+ };
16018
+ }),
16019
+ intents: rows.intents.map((i2) => ({ from: i2.from_unit, to: i2.to_unit, addedAt: i2.added_at })),
16020
+ open: rows.open.map((r2) => ({ from: r2.from_unit, to: r2.to_unit }))
16021
+ };
16022
+ return { snapshot: snapshot2, keys, missing };
16023
+ }
16024
+ function waitText(j, id, l, target) {
16025
+ const w = j.waits.get(id) ?? j.unstood.get(id);
16026
+ const name = (u3) => u3 === undefined ? "its target" : l.keys.get(u3) ?? `u${u3}`;
16027
+ switch (w?.why) {
16028
+ case "unsound":
16029
+ return "its quote was not found or its source is gone, so it can never become active";
16030
+ case "no owner adoption":
16031
+ return "replacing another record needs the owner's or a maintainer's adoption";
16032
+ case "place held":
16033
+ return `waits: ${name(w.holder)} is in effect as the successor of ${name(target)}`;
16034
+ case "target withdrawn":
16035
+ return `${name(target)} is withdrawn, so there is nothing to replace`;
16036
+ case "target quarantined":
16037
+ return `${name(target)} is quarantined (its quote was never found), so it cannot be replaced`;
16038
+ case "kinds":
16039
+ return `${name(target)} is of a kind this record cannot replace`;
16040
+ default:
16041
+ return l.missing.get(id) ?? "its support is not complete";
16042
+ }
16043
+ }
16044
+ function planOf(rows, asked) {
16045
+ const withdraw = new Map(asked.withdraw ?? []);
16046
+ const asking = [...withdraw.keys()];
16047
+ const redundant = asking.filter((id) => judge(snapshotOf(rows, new Set(asking.filter((other) => other !== id))).snapshot).lifecycle.get(id) === "superseded");
16048
+ for (const id of redundant)
16049
+ withdraw.delete(id);
16050
+ const loaded = snapshotOf(rows, new Set(withdraw.keys()));
16051
+ const judged = judge(loaded.snapshot);
16052
+ const plan = difference(loaded.snapshot, judged);
16053
+ const { keys } = loaded;
16054
+ const intentOf = new Map(loaded.snapshot.intents.map((i2) => [i2.from, i2.to]));
16055
+ const writes2 = [];
16056
+ const ended = new Map;
16057
+ for (const r2 of plan.close) {
16058
+ const reason2 = judged.lifecycle.get(r2.from) === "withdrawn" ? `${keys.get(r2.from)} was withdrawn` : judged.holders.has(r2.to) ? `${keys.get(judged.holders.get(r2.to))} took its place` : `${keys.get(r2.from)} no longer stands: ${waitText(judged, r2.from, loaded, r2.to)}`;
16059
+ writes2.push({ op: "close", from: r2.from, to: r2.to, reason: reason2 });
16060
+ ended.set(r2.to, r2.from);
16061
+ }
16062
+ for (const r2 of plan.open)
16063
+ writes2.push({ op: "open", from: r2.from, to: r2.to });
16064
+ for (const t2 of plan.transitions) {
16065
+ const hint = asked.hints?.get(t2.unit);
16066
+ const holder = judged.holders.get(t2.unit);
16067
+ const gone = ended.get(t2.unit);
16068
+ const reason2 = t2.from === null ? "first judged" : t2.to === "withdrawn" ? withdraw.get(t2.unit)?.reason ?? "withdrawn" : t2.to === "superseded" ? `superseded by ${keys.get(holder)}` : t2.from === "superseded" ? `${gone !== undefined ? keys.get(gone) : "its successor"} no longer replaces it` : t2.to === "active" ? hint?.reason ?? "support complete" : `${asked.because ?? "support lost"}: ${waitText(judged, t2.unit, loaded, intentOf.get(t2.unit))}`;
16069
+ const source = t2.to === "withdrawn" ? withdraw.get(t2.unit)?.source ?? null : t2.to === "superseded" ? asked.hints?.get(holder)?.source ?? null : hint?.source ?? null;
16070
+ writes2.push({ op: "state", unit: t2.unit, from: t2.from, to: t2.to, reason: reason2, source });
16071
+ }
16072
+ return { judged, loaded, writes: writes2, redundant, intentOf };
16073
+ }
16074
+ function proveSettled(rows) {
16075
+ const after = snapshotOf(rows, new Set).snapshot;
16076
+ const left = difference(after, judge(after));
16077
+ if (left.close.length || left.open.length || left.transitions.length)
16078
+ throw new Error(`records did not settle after judging: ${JSON.stringify(left)}`);
16079
+ }
16080
+ var UNITS_SQL = `select u.id, u.key, u.kind, u.lifecycle, u.extraction, u.unsourced, s.missing,
16081
+ exists (select 1 from unit_state t where t.unit_id = u.id) as stated,
16082
+ exists (select 1 from unit_state t where t.unit_id = u.id and t.to_state = 'active') as ever,
16083
+ exists (select 1 from unit_adoption a where a.unit_id = u.id and a.route in ('owner_statement', 'explicit')
16084
+ and a.retracted_at is null) as owned,
16085
+ exists (select 1 from unit_option o where o.unit_id = u.id and o.reconsider_when is not null and not exists (
16086
+ select 1 from unit_evidence e join source src on src.id = e.source_id
16087
+ where e.option_id = o.id and e.role = 'reconsiders' and src.author_kind = 'owner')) as unquoted
16088
+ from unit u left join unit_support s on s.unit_id = u.id where u.project_id = ? order by u.id`;
16089
+ var INTENTS_SQL = `select l.from_unit, l.to_unit, l.added_at from unit_link l join unit u on u.id = l.from_unit
16090
+ where l.kind = 'supersedes' and u.project_id = ?`;
16091
+ var OPEN_SQL = `select r.from_unit, r.to_unit from unit_replacement r join unit u on u.id = r.to_unit
16092
+ where r.ended_at is null and u.project_id = ?`;
16093
+ function settleForMigration(io, runs, at) {
16094
+ const notes = [];
16095
+ for (const [project, run2] of runs) {
16096
+ const rowsNow = () => {
16097
+ const units = io.all(UNITS_SQL, project).filter((u3) => Number(u3.stated) === 1);
16098
+ const ids = new Set(units.map((u3) => u3.id));
16099
+ return {
16100
+ units,
16101
+ intents: io.all(INTENTS_SQL, project).filter((i2) => ids.has(i2.from_unit) && ids.has(i2.to_unit)),
16102
+ open: io.all(OPEN_SQL, project).filter((r2) => ids.has(r2.from_unit) && ids.has(r2.to_unit))
16103
+ };
16104
+ };
16105
+ const planned = planOf(rowsNow(), {});
16106
+ const key = (id) => planned.loaded.keys.get(id) ?? `u${id}`;
16107
+ for (const w of planned.writes) {
16108
+ if (w.op === "close") {
16109
+ io.run("update unit_replacement set ended_at = max(?, started_at), end_reason = ?, end_run_id = ? where from_unit = ? and to_unit = ? and ended_at is null", at, w.reason, run2, w.from, w.to);
16110
+ notes.push([`${key(w.from)} → ${key(w.to)}`, `replacement ended: ${w.reason}`]);
16111
+ } else if (w.op === "open") {
16112
+ io.run("insert into unit_replacement (from_unit, to_unit, run_id, started_at) values (?, ?, ?, ?)", w.from, w.to, run2, at);
16113
+ notes.push([
16114
+ `${key(w.from)} → ${key(w.to)}`,
16115
+ "in effect from this update (earlier history not recorded)"
16116
+ ]);
16117
+ } else {
16118
+ io.run("insert into unit_state (unit_id, from_state, to_state, at, reason, source_id, run_id) values (?, ?, ?, max(?, (select created_at from unit where id = ?)), ?, ?, ?)", w.unit, w.from, w.to, at, w.unit, `revision 10: ${w.reason}`, w.source, run2);
16119
+ notes.push([key(w.unit), `${w.from ?? "no state"} → ${w.to}: ${w.reason}`]);
16120
+ }
16121
+ }
16122
+ proveSettled(rowsNow());
16123
+ }
16124
+ return notes;
16125
+ }
16126
+
15865
16127
  // server/src/admin.ts
15866
16128
  var say = (text) => console.log(indent(text));
15867
16129
  var SCHEMA = () => path9.join(dbDir(), "schema.sql");
@@ -15978,6 +16240,15 @@ function withOwner(file, fn, create = false) {
15978
16240
  raw.close();
15979
16241
  }
15980
16242
  }
16243
+ function judgeEveryRecord(raw) {
16244
+ const runs = new Map(raw.prepare("select project_id, max(id) as id from extraction_run where origin = 'migration' and target = 'revision:10' group by project_id").all().map((r2) => [r2.project_id, r2.id]));
16245
+ const io = {
16246
+ all: (sql2, ...p) => raw.prepare(sql2).all(...p),
16247
+ run: (sql2, ...p) => void raw.prepare(sql2).run(...p)
16248
+ };
16249
+ for (const [item, action] of settleForMigration(io, runs, iso(Date.now())))
16250
+ raw.prepare(`insert into temp.${NOTE} (rule, item, action) values ('judged again by revision 10', ?, ?)`).run(item, action);
16251
+ }
15981
16252
  function migrate(file = dbFile(), dir = MIGRATIONS()) {
15982
16253
  return withOwner(file, (raw) => {
15983
16254
  const from = versionOf(raw);
@@ -16002,6 +16273,8 @@ function migrate(file = dbFile(), dir = MIGRATIONS()) {
16002
16273
  throw new Stop(r2, stops);
16003
16274
  }
16004
16275
  raw.exec(fs8.readFileSync(script, "utf8"));
16276
+ if (r2 === 10)
16277
+ judgeEveryRecord(raw);
16005
16278
  const broken = raw.prepare("pragma foreign_key_check").all().length;
16006
16279
  if (broken)
16007
16280
  throw new Error(`Migration to revision ${r2} left ${plural(broken, "broken reference")}; nothing was changed`);