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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -5
- package/db/migrations/0010.check.sql +11 -0
- package/db/migrations/0010.sql +931 -0
- package/db/schema.sql +195 -54
- package/dist/capture.js +47 -4
- package/dist/cli.js +277 -4
- package/dist/deliver.js +187 -49
- package/dist/mcp-record.js +859 -308
- package/dist/mcp.js +277 -183
- package/hooks/hooks.json +13 -0
- package/package.json +1 -1
- package/skills/export/SKILL.md +3 -2
- package/skills/glean/SKILL.md +1 -1
- package/skills/review/reviewers/precedent.md +6 -1
- package/skills/rules/SKILL.md +5 -2
- package/skills/trace/SKILL.md +43 -7
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 =
|
|
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([
|
|
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`);
|