sphica 0.5.6 → 0.6.0
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 +4 -3
- package/db/migrations/0002.sql +145 -0
- package/db/schema.sql +54 -9
- package/dist/capture.js +61 -19
- package/dist/cli.js +95 -22
- package/dist/deliver.js +300 -258
- package/dist/mcp-record.js +450 -53
- package/dist/mcp.js +7 -8
- package/package.json +1 -1
- package/skills/forget/SKILL.md +53 -0
- package/skills/forget/agents/openai.yaml +2 -0
- package/skills/glean/SKILL.md +2 -2
- package/skills/harvest/SKILL.md +1 -1
- package/skills/trace/SKILL.md +1 -1
package/dist/mcp-record.js
CHANGED
|
@@ -34719,9 +34719,6 @@ class StdioServerTransport {
|
|
|
34719
34719
|
}
|
|
34720
34720
|
}
|
|
34721
34721
|
|
|
34722
|
-
// server/src/db-write.ts
|
|
34723
|
-
import { constants as C2, DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
34724
|
-
|
|
34725
34722
|
// server/node_modules/kysely/dist/util/object-utils.js
|
|
34726
34723
|
function isUndefined(obj) {
|
|
34727
34724
|
return typeof obj === "undefined" || obj === undefined;
|
|
@@ -45530,7 +45527,7 @@ import os from "node:os";
|
|
|
45530
45527
|
import path from "node:path";
|
|
45531
45528
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
45532
45529
|
var SCHEMA_GENERATION = 2;
|
|
45533
|
-
var SCHEMA_REVISION =
|
|
45530
|
+
var SCHEMA_REVISION = 2;
|
|
45534
45531
|
var sphicaHome = () => process.env.SPHICA_HOME || path.join(os.homedir(), ".sphica");
|
|
45535
45532
|
var dbFile = () => process.env.SPHICA_DB || path.join(sphicaHome(), "sphica.db");
|
|
45536
45533
|
function requireRuntime() {
|
|
@@ -45554,7 +45551,7 @@ function prepare(raw, check2) {
|
|
|
45554
45551
|
const got = raw.prepare("pragma user_version").get()?.user_version;
|
|
45555
45552
|
if (got === SCHEMA_REVISION)
|
|
45556
45553
|
return;
|
|
45557
|
-
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "
|
|
45554
|
+
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "Update the sphica CLI (`npm i -g sphica`), then run `sphica init` to migrate it (records are kept)." : "Update sphica."));
|
|
45558
45555
|
}
|
|
45559
45556
|
function generationOf(raw) {
|
|
45560
45557
|
const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
|
|
@@ -45626,6 +45623,9 @@ function sqliteCode(e) {
|
|
|
45626
45623
|
return x?.code === "ERR_SQLITE_ERROR" && typeof x.errcode === "number" ? x.errcode & 255 : null;
|
|
45627
45624
|
}
|
|
45628
45625
|
|
|
45626
|
+
// server/src/db-write.ts
|
|
45627
|
+
import { constants as C2, DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
45628
|
+
|
|
45629
45629
|
// server/src/text.ts
|
|
45630
45630
|
import crypto from "node:crypto";
|
|
45631
45631
|
var segmenter = new Intl.Segmenter("ja", { granularity: "word" });
|
|
@@ -45822,27 +45822,80 @@ var MYSQL_PASSWORD = /(\s-p)(?:'[^'\n]*'|"[^"\n]*"|(?=[^\s-])\S+)/;
|
|
|
45822
45822
|
var URL_CREDENTIALS = /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|https?):\/\/[^:\s/@]*:)[^\s/]*@([^@\s/?#]+)/g;
|
|
45823
45823
|
var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
|
|
45824
45824
|
var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
45825
|
-
function
|
|
45825
|
+
function keyBlocks(text) {
|
|
45826
45826
|
const ends = [...text.matchAll(KEY_END)].map((m) => [m.index, m.index + m[0].length]);
|
|
45827
|
-
|
|
45828
|
-
return text;
|
|
45829
|
-
let out = "";
|
|
45827
|
+
const out = [];
|
|
45830
45828
|
let last = 0;
|
|
45831
45829
|
let e = 0;
|
|
45832
|
-
|
|
45833
|
-
const
|
|
45834
|
-
|
|
45835
|
-
|
|
45836
|
-
|
|
45837
|
-
e
|
|
45838
|
-
|
|
45839
|
-
|
|
45840
|
-
|
|
45841
|
-
|
|
45842
|
-
|
|
45830
|
+
if (ends.length > 0)
|
|
45831
|
+
for (const m of text.matchAll(KEY_BEGIN)) {
|
|
45832
|
+
const after = m.index + m[0].length;
|
|
45833
|
+
if (m.index < last)
|
|
45834
|
+
continue;
|
|
45835
|
+
while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
|
|
45836
|
+
e++;
|
|
45837
|
+
const end = ends[e];
|
|
45838
|
+
if (!end)
|
|
45839
|
+
break;
|
|
45840
|
+
out.push([m.index, end[1]]);
|
|
45841
|
+
last = end[1];
|
|
45842
|
+
}
|
|
45843
|
+
return out;
|
|
45844
|
+
}
|
|
45845
|
+
function maskPrivateKeys(text) {
|
|
45846
|
+
let out = "";
|
|
45847
|
+
let last = 0;
|
|
45848
|
+
for (const [a, b] of keyBlocks(text)) {
|
|
45849
|
+
out += `${text.slice(last, a)}[redacted: private key]`;
|
|
45850
|
+
last = b;
|
|
45843
45851
|
}
|
|
45844
45852
|
return out + text.slice(last);
|
|
45845
45853
|
}
|
|
45854
|
+
function privateKeyRanges(text) {
|
|
45855
|
+
return byteRanges(text, keyBlocks(text));
|
|
45856
|
+
}
|
|
45857
|
+
function byteRanges(text, ranges) {
|
|
45858
|
+
const out = [];
|
|
45859
|
+
let at = 0;
|
|
45860
|
+
let pos = 0;
|
|
45861
|
+
for (const [a, b] of ranges) {
|
|
45862
|
+
pos += bytes(text.slice(at, a));
|
|
45863
|
+
const end = pos + bytes(text.slice(a, b));
|
|
45864
|
+
out.push([pos, end]);
|
|
45865
|
+
at = b;
|
|
45866
|
+
pos = end;
|
|
45867
|
+
}
|
|
45868
|
+
return out;
|
|
45869
|
+
}
|
|
45870
|
+
var PLACEHOLDER = /\[redacted(?:: [^\]\n]{1,40})?\]/g;
|
|
45871
|
+
function placeholderRanges(masked) {
|
|
45872
|
+
return [...masked.matchAll(PLACEHOLDER)].map((x) => [x.index, x.index + x[0].length]);
|
|
45873
|
+
}
|
|
45874
|
+
function starts(hay, needle) {
|
|
45875
|
+
const out = [];
|
|
45876
|
+
for (let at = hay.indexOf(needle);at >= 0; at = hay.indexOf(needle, at + 1))
|
|
45877
|
+
out.push(at);
|
|
45878
|
+
return out;
|
|
45879
|
+
}
|
|
45880
|
+
function quoteSpan(raw, masked, quote) {
|
|
45881
|
+
const q = Buffer.from(quote, "utf8");
|
|
45882
|
+
if (q.length === 0)
|
|
45883
|
+
return null;
|
|
45884
|
+
const m = Buffer.from(masked, "utf8");
|
|
45885
|
+
if (raw === masked) {
|
|
45886
|
+
const at = m.indexOf(q);
|
|
45887
|
+
return at < 0 ? null : [at, at + q.length];
|
|
45888
|
+
}
|
|
45889
|
+
const holes = byteRanges(masked, placeholderRanges(masked));
|
|
45890
|
+
let h = 0;
|
|
45891
|
+
const kept = starts(m, q).filter((at) => {
|
|
45892
|
+
while ((holes[h]?.[1] ?? Number.POSITIVE_INFINITY) <= at)
|
|
45893
|
+
h++;
|
|
45894
|
+
return !((holes[h]?.[0] ?? Number.POSITIVE_INFINITY) < at + q.length);
|
|
45895
|
+
});
|
|
45896
|
+
const first = kept[0];
|
|
45897
|
+
return first !== undefined && kept.length === starts(Buffer.from(raw, "utf8"), q).length ? [first, first + q.length] : null;
|
|
45898
|
+
}
|
|
45846
45899
|
function mask(text) {
|
|
45847
45900
|
let out = maskFields(maskPrivateKeys(text).replace(URL_CREDENTIALS, "$1[redacted]@$2").replace(AUTH_HEADER, "$1[redacted]").replace(HEADER_BEARER, "$1[redacted]").replace(ENV_ASSIGN, "$1$2[redacted]")).replace(MYSQL_COMMAND, (command) => command.replace(MYSQL_PASSWORD, "$1[redacted]"));
|
|
45848
45901
|
for (const [re, what] of SECRETS)
|
|
@@ -45940,22 +45993,54 @@ function captureAuthorizer(own2, action, p1, p2, triggerOrView) {
|
|
|
45940
45993
|
return C2.SQLITE_DENY;
|
|
45941
45994
|
}
|
|
45942
45995
|
var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
|
|
45996
|
+
var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
|
|
45997
|
+
var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
|
|
45943
45998
|
function ingestAuthorizer(action, p1, p2) {
|
|
45944
45999
|
if (DDL().has(action))
|
|
45945
46000
|
return C2.SQLITE_DENY;
|
|
45946
|
-
if (writes(action) && p1 === "owner_identity")
|
|
46001
|
+
if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
|
|
46002
|
+
return C2.SQLITE_DENY;
|
|
46003
|
+
if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
|
|
45947
46004
|
return C2.SQLITE_DENY;
|
|
45948
46005
|
if (action === C2.SQLITE_PRAGMA)
|
|
45949
46006
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
45950
46007
|
return C2.SQLITE_OK;
|
|
45951
46008
|
}
|
|
46009
|
+
var FORGET_WRITES = {
|
|
46010
|
+
[C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts"]),
|
|
46011
|
+
[C2.SQLITE_DELETE]: new Set([
|
|
46012
|
+
"source",
|
|
46013
|
+
"external_reference",
|
|
46014
|
+
"unit_evidence",
|
|
46015
|
+
"unit_adoption",
|
|
46016
|
+
"source_processing",
|
|
46017
|
+
"source_fts"
|
|
46018
|
+
])
|
|
46019
|
+
};
|
|
46020
|
+
var FORGET_UPDATES = {
|
|
46021
|
+
unit: new Set(["lifecycle", "revision"]),
|
|
46022
|
+
unit_state: new Set(["source_id"])
|
|
46023
|
+
};
|
|
46024
|
+
function forgetAuthorizer(action, p1, p2) {
|
|
46025
|
+
if (SHADOW.test(p1 ?? "") && writes(action))
|
|
46026
|
+
return C2.SQLITE_OK;
|
|
46027
|
+
if (action === C2.SQLITE_UPDATE)
|
|
46028
|
+
return FORGET_UPDATES[p1 ?? ""]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46029
|
+
if (action === C2.SQLITE_INSERT || action === C2.SQLITE_DELETE)
|
|
46030
|
+
return FORGET_WRITES[action]?.has(p1 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46031
|
+
if (action === C2.SQLITE_PRAGMA)
|
|
46032
|
+
return readsDataVersion(p1, p2) || p1 === "secure_delete" && ["on", "1", "true"].includes((p2 ?? "").toLowerCase()) || p1 === "wal_checkpoint" ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46033
|
+
if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
46034
|
+
return C2.SQLITE_OK;
|
|
46035
|
+
return C2.SQLITE_DENY;
|
|
46036
|
+
}
|
|
45952
46037
|
function connectWriter(role, file2 = dbFile(), create = false) {
|
|
45953
46038
|
requireRuntime();
|
|
45954
46039
|
if (!create)
|
|
45955
46040
|
requireFile(file2);
|
|
45956
46041
|
const raw = new DatabaseSync2(file2);
|
|
45957
46042
|
try {
|
|
45958
|
-
prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
|
|
46043
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
45959
46044
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
45960
46045
|
} catch (e) {
|
|
45961
46046
|
raw.close();
|
|
@@ -45963,6 +46048,8 @@ function connectWriter(role, file2 = dbFile(), create = false) {
|
|
|
45963
46048
|
}
|
|
45964
46049
|
if (role === "ingest")
|
|
45965
46050
|
raw.setAuthorizer(ingestAuthorizer);
|
|
46051
|
+
else if (role === "forget")
|
|
46052
|
+
raw.setAuthorizer(forgetAuthorizer);
|
|
45966
46053
|
else if (role === "capture") {
|
|
45967
46054
|
let own2 = false;
|
|
45968
46055
|
raw.setAuthorizer((action, p1, p2, _db, triggerOrView) => captureAuthorizer(own2, action, p1, p2, triggerOrView));
|
|
@@ -46053,14 +46140,61 @@ function readText(root, rel) {
|
|
|
46053
46140
|
function findSymbol(text, symbol2) {
|
|
46054
46141
|
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`);
|
|
46055
46142
|
const lines = text.split(/\r?\n/);
|
|
46056
|
-
|
|
46057
|
-
|
|
46143
|
+
return { lines, i: lines.findIndex((l) => re.test(l)) };
|
|
46144
|
+
}
|
|
46145
|
+
function swallowed(root, rel, symbol2) {
|
|
46146
|
+
if (mask(symbol2) !== symbol2)
|
|
46147
|
+
return true;
|
|
46148
|
+
const text = root ? readText(root, rel) : null;
|
|
46149
|
+
if (text === undefined)
|
|
46150
|
+
return true;
|
|
46151
|
+
if (text === null)
|
|
46152
|
+
return false;
|
|
46153
|
+
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`, "g");
|
|
46154
|
+
let raw = 0;
|
|
46155
|
+
for (let m = re.exec(text);m; m = re.exec(text))
|
|
46156
|
+
raw++;
|
|
46157
|
+
if (raw === 0)
|
|
46158
|
+
return false;
|
|
46159
|
+
const masked = mask(text);
|
|
46160
|
+
const holes = placeholderRanges(masked);
|
|
46161
|
+
let h = 0;
|
|
46162
|
+
let kept = 0;
|
|
46163
|
+
re.lastIndex = 0;
|
|
46164
|
+
for (let m = re.exec(masked);m; m = re.exec(masked)) {
|
|
46165
|
+
while ((holes[h]?.[1] ?? Number.POSITIVE_INFINITY) < m.index)
|
|
46166
|
+
h++;
|
|
46167
|
+
if (!((holes[h]?.[0] ?? Number.POSITIVE_INFINITY) <= m.index + symbol2.length))
|
|
46168
|
+
kept++;
|
|
46169
|
+
}
|
|
46170
|
+
return kept !== raw;
|
|
46171
|
+
}
|
|
46172
|
+
function masksSymbol(root, rel, symbol2) {
|
|
46173
|
+
const trimmed = symbol2.trim();
|
|
46174
|
+
return swallowed(root, rel, symbol2) || trimmed !== symbol2 && trimmed !== "" && swallowed(root, rel, trimmed);
|
|
46058
46175
|
}
|
|
46059
46176
|
function locate(root, rel, symbol2) {
|
|
46060
46177
|
if (!root)
|
|
46061
46178
|
return null;
|
|
46062
46179
|
const text = readText(root, rel);
|
|
46063
|
-
|
|
46180
|
+
if (typeof text !== "string")
|
|
46181
|
+
return null;
|
|
46182
|
+
const { lines, i } = findSymbol(text, symbol2);
|
|
46183
|
+
if (i < 0)
|
|
46184
|
+
return null;
|
|
46185
|
+
const line = lines[i] ?? "";
|
|
46186
|
+
const from = bytes(lines.slice(0, i).join(`
|
|
46187
|
+
`)) + (i > 0 ? 1 : 0);
|
|
46188
|
+
if (privateKeyRanges(lines.join(`
|
|
46189
|
+
`)).some(([a, b]) => a < from + bytes(line) && b > from))
|
|
46190
|
+
return { line: i + 1, excerpt: "[redacted: private key]" };
|
|
46191
|
+
const before = lines.slice(0, i).map((l) => `${l}
|
|
46192
|
+
`).join("");
|
|
46193
|
+
const after = lines.slice(i + 1).map((l) => `
|
|
46194
|
+
${l}`).join("");
|
|
46195
|
+
const own2 = mask(line);
|
|
46196
|
+
const cut = mask(before + line) !== mask(before) + own2 || mask(line + after) !== own2 + mask(after);
|
|
46197
|
+
return { line: i + 1, excerpt: cut ? "[redacted]" : own2.trim().slice(0, 200) };
|
|
46064
46198
|
}
|
|
46065
46199
|
|
|
46066
46200
|
// server/src/project.ts
|
|
@@ -46940,11 +47074,13 @@ async function storeItems(db, projectId2, items) {
|
|
|
46940
47074
|
const kept = fit(it.text);
|
|
46941
47075
|
const hash2 = sha256(kept.body);
|
|
46942
47076
|
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();
|
|
46943
|
-
|
|
47077
|
+
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();
|
|
47078
|
+
const lastForgotten = Math.max(0, ...forgotten.map((f) => f.revision));
|
|
47079
|
+
if (latest && latest.revision > lastForgotten && Buffer.from(latest.content_hash).equals(hash2)) {
|
|
46944
47080
|
ids.push(latest.id);
|
|
46945
47081
|
continue;
|
|
46946
47082
|
}
|
|
46947
|
-
if (!latest && !kept.body.trim()) {
|
|
47083
|
+
if (forgotten.some((f) => Buffer.from(f.content_hash).equals(hash2)) || !latest && !kept.body.trim()) {
|
|
46948
47084
|
ids.push(null);
|
|
46949
47085
|
continue;
|
|
46950
47086
|
}
|
|
@@ -46957,7 +47093,7 @@ async function storeItems(db, projectId2, items) {
|
|
|
46957
47093
|
kind: it.kind,
|
|
46958
47094
|
artifact: it.artifact,
|
|
46959
47095
|
external_id: it.externalId,
|
|
46960
|
-
revision: (latest?.revision ?? 0) + 1,
|
|
47096
|
+
revision: Math.max(latest?.revision ?? 0, lastForgotten) + 1,
|
|
46961
47097
|
author_kind: kind,
|
|
46962
47098
|
author_login: it.author?.login ?? null,
|
|
46963
47099
|
author_external_id: authorId,
|
|
@@ -46966,7 +47102,7 @@ async function storeItems(db, projectId2, items) {
|
|
|
46966
47102
|
event_kind: it.event,
|
|
46967
47103
|
url: it.url,
|
|
46968
47104
|
created_at: created,
|
|
46969
|
-
available_at: latest ? null : created,
|
|
47105
|
+
available_at: latest || lastForgotten ? null : created,
|
|
46970
47106
|
captured_at: now,
|
|
46971
47107
|
text: kept.body,
|
|
46972
47108
|
truncated: kept.truncated ? 1 : 0,
|
|
@@ -47002,7 +47138,7 @@ async function pullSources(db, projectId2, number5) {
|
|
|
47002
47138
|
`pr:${number5}`,
|
|
47003
47139
|
...(await db.selectFrom("artifact_link").select("to_artifact").where("project_id", "=", projectId2).where("from_artifact", "=", `pr:${number5}`).execute()).map((l) => l.to_artifact)
|
|
47004
47140
|
];
|
|
47005
|
-
return db.selectFrom("source as s").where("s.project_id", "=", projectId2).where("s.artifact", "in", artifacts).where(({ not, exists, selectFrom }) => not(exists(selectFrom("source as n").select("n.id").whereRef("n.project_id", "=", "s.project_id").whereRef("n.kind", "=", "s.kind").whereRef("n.external_id", "=", "s.external_id").whereRef("n.revision", ">", "s.revision")))).select([
|
|
47141
|
+
return db.selectFrom("source as s").where("s.project_id", "=", projectId2).where("s.artifact", "in", artifacts).where(({ not, exists, selectFrom }) => not(exists(selectFrom("source as n").select("n.id").whereRef("n.project_id", "=", "s.project_id").whereRef("n.kind", "=", "s.kind").whereRef("n.external_id", "=", "s.external_id").whereRef("n.revision", ">", "s.revision")))).where(({ not, exists, selectFrom }) => not(exists(selectFrom("source_forgotten as f").select("f.source_id").whereRef("f.project_id", "=", "s.project_id").whereRef("f.kind", "=", "s.kind").whereRef("f.external_id", "=", "s.external_id").whereRef("f.revision", ">", "s.revision")))).select([
|
|
47006
47142
|
"s.id",
|
|
47007
47143
|
"s.kind",
|
|
47008
47144
|
"s.artifact",
|
|
@@ -47212,19 +47348,36 @@ async function checkRecord(db, target, raw) {
|
|
|
47212
47348
|
adoption.push({ source: s.id, start: span[0], end: span[1], route });
|
|
47213
47349
|
}
|
|
47214
47350
|
const anchors = [];
|
|
47351
|
+
const fallbacks = new Set;
|
|
47215
47352
|
for (const a of u.anchors) {
|
|
47216
47353
|
const p = repoPath(a.path);
|
|
47217
47354
|
if (!p) {
|
|
47218
47355
|
problems.push(`${key}: anchor path ${JSON.stringify(head(a.path, 80))} is not inside the repository; left out`);
|
|
47219
47356
|
continue;
|
|
47220
47357
|
}
|
|
47358
|
+
const symbol2 = a.symbol && masksSymbol(target.root, p, a.symbol) ? undefined : a.symbol;
|
|
47359
|
+
if (a.symbol && !symbol2)
|
|
47360
|
+
problems.push(`${key}: anchor symbol in ${p} is text Sphica masks; the anchor keeps only its path`);
|
|
47221
47361
|
let commit = a.commit;
|
|
47222
47362
|
if (commit && !(target.root && commitHolds(target.root, commit, p))) {
|
|
47223
47363
|
problems.push(`${key}: commit ${commit.slice(0, 12)} does not hold ${p} in the repository${target.root ? "" : " (no working tree to check)"}; the anchor keeps no commit`);
|
|
47224
47364
|
commit = undefined;
|
|
47225
47365
|
}
|
|
47226
47366
|
const observation = a.role === "evidence" && !commit && target.sessionId ? (await db.selectFrom("edit_observation").select("id").where("session_id", "=", target.sessionId).where("path", "=", p).orderBy("id", "desc").executeTakeFirst())?.id ?? null : null;
|
|
47227
|
-
|
|
47367
|
+
const planned = { ...a, symbol: symbol2, commit, path: p, observation };
|
|
47368
|
+
if (a.symbol && !symbol2)
|
|
47369
|
+
fallbacks.add(planned);
|
|
47370
|
+
anchors.push(planned);
|
|
47371
|
+
}
|
|
47372
|
+
const place = (x) => `${x.path}\x00${x.role}\x00${x.commit}\x00${x.lines ? [x.lines[0], Math.max(...x.lines)] : ""}`;
|
|
47373
|
+
const covered = new Set(anchors.filter((x) => !x.symbol && !fallbacks.has(x)).map(place));
|
|
47374
|
+
for (const x of [...fallbacks]) {
|
|
47375
|
+
if (!covered.has(place(x))) {
|
|
47376
|
+
covered.add(place(x));
|
|
47377
|
+
continue;
|
|
47378
|
+
}
|
|
47379
|
+
problems.push(`${key}: another path-only anchor on ${x.path} already covers it; left out`);
|
|
47380
|
+
anchors.splice(anchors.indexOf(x), 1);
|
|
47228
47381
|
}
|
|
47229
47382
|
const aliases = [...new Set(u.aliases.map((a) => a.trim()))];
|
|
47230
47383
|
const bad = aliases.filter((a) => !a || a.length > 40);
|
|
@@ -47346,12 +47499,13 @@ ${checked.errors.map((e) => ` ${e}`).join(`
|
|
|
47346
47499
|
}).onConflict((oc) => oc.doNothing()).execute();
|
|
47347
47500
|
}
|
|
47348
47501
|
for (const a of p.anchors) {
|
|
47349
|
-
const
|
|
47502
|
+
const symbol2 = a.symbol && !masksSymbol(target.root, a.path, a.symbol) ? a.symbol : undefined;
|
|
47503
|
+
const at = a.lines ? null : symbol2 ? locate(target.root, a.path, symbol2) : null;
|
|
47350
47504
|
const lines = a.lines ?? (at ? [at.line, at.line] : null);
|
|
47351
47505
|
await trx.insertInto("unit_anchor").values({
|
|
47352
47506
|
unit_id: id,
|
|
47353
47507
|
path: a.path,
|
|
47354
|
-
symbol:
|
|
47508
|
+
symbol: symbol2 ?? null,
|
|
47355
47509
|
commit_sha: a.commit ?? null,
|
|
47356
47510
|
line_start: lines?.[0] ?? null,
|
|
47357
47511
|
line_end: lines ? Math.max(lines[0], lines[1]) : null,
|
|
@@ -47549,15 +47703,24 @@ function readExcerpt(root, file2) {
|
|
|
47549
47703
|
} catch {
|
|
47550
47704
|
throw new Error(`${p} is not UTF-8 text`);
|
|
47551
47705
|
}
|
|
47552
|
-
const
|
|
47706
|
+
const starts2 = [0];
|
|
47553
47707
|
for (let i = 0;i < buf.length; i++)
|
|
47554
47708
|
if (buf[i] === 10 && i + 1 < buf.length)
|
|
47555
|
-
|
|
47709
|
+
starts2.push(i + 1);
|
|
47556
47710
|
const [a, b] = file2.lines;
|
|
47557
|
-
if (a > b || b >
|
|
47558
|
-
throw new Error(`${p} has ${
|
|
47559
|
-
const
|
|
47560
|
-
|
|
47711
|
+
if (a > b || b > starts2.length)
|
|
47712
|
+
throw new Error(`${p} has ${starts2.length} lines; lines ${a}-${b} are not in it`);
|
|
47713
|
+
const from = starts2[a - 1] ?? 0;
|
|
47714
|
+
const to = b < starts2.length ? starts2[b] ?? buf.length : buf.length;
|
|
47715
|
+
if (privateKeyRanges(buf.toString("utf8")).some(([s, e]) => s < to && e > from && (s < from || e > to)))
|
|
47716
|
+
throw new Error(`${p} lines ${a}-${b} are inside a private key; cite lines outside it`);
|
|
47717
|
+
const raw = buf.subarray(from, to).toString("utf8");
|
|
47718
|
+
const text2 = mask(raw);
|
|
47719
|
+
const before = buf.subarray(0, from).toString("utf8");
|
|
47720
|
+
const after = buf.subarray(to).toString("utf8");
|
|
47721
|
+
if (mask(before + raw) !== mask(before) + text2 || mask(raw + after) !== text2 + mask(after))
|
|
47722
|
+
throw new Error(`${p} lines ${a}-${b} cut through text Sphica masks; cite more lines around it`);
|
|
47723
|
+
return { path: p, commit, blob, size, lines: [a, b], raw, text: text2 };
|
|
47561
47724
|
}
|
|
47562
47725
|
var locate3 = (body, q) => {
|
|
47563
47726
|
const at = Buffer.from(body, "utf8").indexOf(Buffer.from(q, "utf8"));
|
|
@@ -47621,8 +47784,12 @@ async function checkGlean(db, target, raw) {
|
|
|
47621
47784
|
if (!target.root)
|
|
47622
47785
|
throw new Error("the repository is not known");
|
|
47623
47786
|
excerpt = readExcerpt(target.root, op.file);
|
|
47624
|
-
if (
|
|
47787
|
+
if ((await forgottenExcerpt(db, target.projectId, excerpt)).same)
|
|
47788
|
+
errors3.push(`${what}: the owner forgot ${excerpt.path} lines ${op.file.lines.join("-")}; cite something else`);
|
|
47789
|
+
else if (!locate3(excerpt.raw, op.quote))
|
|
47625
47790
|
errors3.push(`${what}: quote not found in ${excerpt.path} lines ${op.file.lines.join("-")}`);
|
|
47791
|
+
else if (!quoteSpan(excerpt.raw, excerpt.text, op.quote))
|
|
47792
|
+
errors3.push(`${what}: the quote also appears in text Sphica masks; quote a longer or different part`);
|
|
47626
47793
|
} catch (e) {
|
|
47627
47794
|
errors3.push(`${what}: ${e.message}`);
|
|
47628
47795
|
}
|
|
@@ -47643,6 +47810,10 @@ async function checkGlean(db, target, raw) {
|
|
|
47643
47810
|
}
|
|
47644
47811
|
if (op.op === "anchor" && !repoPath(op.path))
|
|
47645
47812
|
errors3.push(`${what}: the path is not inside the repository`);
|
|
47813
|
+
const pinned = op.op === "anchor" ? op : op.op === "replace_anchor" ? op.to : null;
|
|
47814
|
+
const pinnedPath = pinned && repoPath(pinned.path);
|
|
47815
|
+
if (pinned?.symbol && pinnedPath && masksSymbol(target.root, pinnedPath, pinned.symbol))
|
|
47816
|
+
errors3.push(`${what}: the symbol is text Sphica masks; anchor a name, not a key`);
|
|
47646
47817
|
if (op.op === "anchor" && op.commit) {
|
|
47647
47818
|
const rel2 = repoPath(op.path);
|
|
47648
47819
|
if (rel2 && !(target.root && commitHolds(target.root, op.commit, rel2)))
|
|
@@ -47730,26 +47901,38 @@ async function checkGlean(db, target, raw) {
|
|
|
47730
47901
|
}
|
|
47731
47902
|
return { errors: errors3, problems, units, ops };
|
|
47732
47903
|
}
|
|
47904
|
+
async function forgottenExcerpt(db, projectId2, x) {
|
|
47905
|
+
const rows = await db.selectFrom("source_forgotten").select(["revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", "file_excerpt").where("external_id", "=", `file:${x.path}@${x.blob}#L${x.lines[0]}-${x.lines[1]}`).execute();
|
|
47906
|
+
const hash2 = sha256(x.text);
|
|
47907
|
+
return {
|
|
47908
|
+
same: rows.some((r) => Buffer.from(r.content_hash).equals(hash2)),
|
|
47909
|
+
last: Math.max(0, ...rows.map((r) => r.revision))
|
|
47910
|
+
};
|
|
47911
|
+
}
|
|
47733
47912
|
async function excerptSource(trx, projectId2, x) {
|
|
47734
47913
|
const external2 = `file:${x.path}@${x.blob}#L${x.lines[0]}-${x.lines[1]}`;
|
|
47735
|
-
const
|
|
47736
|
-
|
|
47914
|
+
const hash2 = sha256(x.text);
|
|
47915
|
+
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();
|
|
47916
|
+
const forgotten = await forgottenExcerpt(trx, projectId2, x);
|
|
47917
|
+
if (found && found.revision > forgotten.last && found.content_hash.equals(hash2))
|
|
47737
47918
|
return found.id;
|
|
47919
|
+
if (forgotten.same)
|
|
47920
|
+
throw new Error(`the owner forgot ${x.path} lines ${x.lines.join("-")}; cite something else`);
|
|
47738
47921
|
const now = iso(Date.now());
|
|
47739
|
-
const partial2 = Buffer.byteLength(x.text, "utf8") !== x.size;
|
|
47740
47922
|
const row = await trx.insertInto("source").values({
|
|
47741
47923
|
project_id: projectId2,
|
|
47742
47924
|
kind: "file_excerpt",
|
|
47743
47925
|
artifact: `file:${x.path}`,
|
|
47744
47926
|
external_id: external2,
|
|
47745
|
-
revision: 1,
|
|
47927
|
+
revision: Math.max(found?.revision ?? 0, forgotten.last) + 1,
|
|
47746
47928
|
author_kind: "person",
|
|
47747
47929
|
created_at: now,
|
|
47748
47930
|
captured_at: now,
|
|
47749
47931
|
text: x.text,
|
|
47750
|
-
truncated:
|
|
47932
|
+
truncated: bytes(x.raw) !== x.size ? 1 : 0,
|
|
47933
|
+
redacted: x.text !== x.raw ? 1 : 0,
|
|
47751
47934
|
original_bytes: x.size,
|
|
47752
|
-
content_hash:
|
|
47935
|
+
content_hash: hash2,
|
|
47753
47936
|
path: x.path,
|
|
47754
47937
|
line_start: x.lines[0],
|
|
47755
47938
|
line_end: x.lines[1],
|
|
@@ -47799,7 +47982,10 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
47799
47982
|
touched.set(p.unitId, op.unit);
|
|
47800
47983
|
if (op.op === "add_evidence") {
|
|
47801
47984
|
const s = p.excerpt ? { id: await excerptSource(trx, target.projectId, p.excerpt), text: p.excerpt.text } : await trx.selectFrom("source").select(["id", "text"]).where("id", "=", Number(op.source?.slice(1))).executeTakeFirstOrThrow();
|
|
47802
|
-
const at = locate3(s.text, op.quote) ?? [
|
|
47985
|
+
const at = (p.excerpt ? quoteSpan(p.excerpt.raw, s.text, op.quote) : locate3(s.text, op.quote)) ?? [
|
|
47986
|
+
0,
|
|
47987
|
+
0
|
|
47988
|
+
];
|
|
47803
47989
|
await trx.insertInto("unit_evidence").values({
|
|
47804
47990
|
unit_id: p.unitId,
|
|
47805
47991
|
source_id: s.id,
|
|
@@ -47826,11 +48012,13 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
47826
48012
|
changed2.push(`${op.unit}: adopted`);
|
|
47827
48013
|
} else if (op.op === "anchor" || op.op === "replace_anchor") {
|
|
47828
48014
|
const to = op.op === "anchor" ? op : op.to;
|
|
47829
|
-
const
|
|
48015
|
+
const rel = repoPath(to.path) ?? to.path;
|
|
48016
|
+
const symbol2 = to.symbol && !masksSymbol(target.root, rel, to.symbol) ? to.symbol : null;
|
|
48017
|
+
const at = symbol2 ? locate(target.root, rel, symbol2) : null;
|
|
47830
48018
|
const added = await trx.insertInto("unit_anchor").values({
|
|
47831
48019
|
unit_id: p.unitId,
|
|
47832
|
-
path:
|
|
47833
|
-
symbol:
|
|
48020
|
+
path: rel,
|
|
48021
|
+
symbol: symbol2,
|
|
47834
48022
|
commit_sha: op.op === "anchor" ? op.commit ?? null : null,
|
|
47835
48023
|
line_start: at?.line ?? null,
|
|
47836
48024
|
line_end: at?.line ?? null,
|
|
@@ -47988,7 +48176,7 @@ async function beginHarvest(db, projectId2, number5, get) {
|
|
|
47988
48176
|
const project = await db.selectFrom("project").select("key").where("id", "=", projectId2).executeTakeFirst();
|
|
47989
48177
|
const pull = await readPull(get, number5, repoOf(project?.key ?? ""));
|
|
47990
48178
|
return inTransaction(db, async (trx) => {
|
|
47991
|
-
await storeItems(trx, projectId2, pull.items);
|
|
48179
|
+
const kept = (await storeItems(trx, projectId2, pull.items)).filter((id) => id !== null).length;
|
|
47992
48180
|
await linkIssues(trx, projectId2, number5, pull.closes);
|
|
47993
48181
|
const run = newRunId();
|
|
47994
48182
|
await openRun(trx, {
|
|
@@ -47998,7 +48186,7 @@ async function beginHarvest(db, projectId2, number5, get) {
|
|
|
47998
48186
|
sessionId: null,
|
|
47999
48187
|
draftId: run
|
|
48000
48188
|
});
|
|
48001
|
-
return { run, sources:
|
|
48189
|
+
return { run, sources: kept };
|
|
48002
48190
|
});
|
|
48003
48191
|
}
|
|
48004
48192
|
async function bound(db, id, projectId2) {
|
|
@@ -48149,6 +48337,153 @@ async function saveText(db, id, projectId2, root, record3) {
|
|
|
48149
48337
|
});
|
|
48150
48338
|
}
|
|
48151
48339
|
|
|
48340
|
+
// server/src/forget.ts
|
|
48341
|
+
class Preview extends Error {
|
|
48342
|
+
outcome;
|
|
48343
|
+
constructor(outcome) {
|
|
48344
|
+
super("preview");
|
|
48345
|
+
this.outcome = outcome;
|
|
48346
|
+
}
|
|
48347
|
+
}
|
|
48348
|
+
var unknown3 = (id) => new Error(`s${id} is not a source of this project`);
|
|
48349
|
+
async function forgetIn(trx, projectId2, ids, at) {
|
|
48350
|
+
const unique = [...new Set(ids)];
|
|
48351
|
+
const rows = unique.length ? await trx.selectFrom("source").where("id", "in", unique).where("project_id", "=", projectId2).select([
|
|
48352
|
+
"id",
|
|
48353
|
+
"kind",
|
|
48354
|
+
"artifact",
|
|
48355
|
+
"external_id",
|
|
48356
|
+
"revision",
|
|
48357
|
+
"content_hash",
|
|
48358
|
+
sql`length(cast(text as blob))`.as("bytes")
|
|
48359
|
+
]).orderBy("id").execute() : [];
|
|
48360
|
+
const gone = unique.length ? await trx.selectFrom("source_forgotten").where("source_id", "in", unique).where("project_id", "=", projectId2).select("source_id").execute() : [];
|
|
48361
|
+
const already = gone.map((g) => g.source_id).sort((a, b) => a - b);
|
|
48362
|
+
for (const id of unique)
|
|
48363
|
+
if (!rows.some((r) => r.id === id) && !already.includes(id))
|
|
48364
|
+
throw unknown3(id);
|
|
48365
|
+
const outcome = {
|
|
48366
|
+
sources: rows.map((r) => ({ id: r.id, kind: r.kind, artifact: r.artifact, bytes: r.bytes })),
|
|
48367
|
+
already,
|
|
48368
|
+
units: [],
|
|
48369
|
+
references: 0
|
|
48370
|
+
};
|
|
48371
|
+
if (!rows.length)
|
|
48372
|
+
return outcome;
|
|
48373
|
+
const targets = rows.map((r) => r.id);
|
|
48374
|
+
const touched = await trx.selectFrom("unit as u").where("u.project_id", "=", projectId2).where((eb) => eb.or(["unit_evidence", "unit_adoption"].flatMap((t) => [
|
|
48375
|
+
eb.exists(eb.selectFrom(t).select(`${t}.id`).whereRef(`${t}.unit_id`, "=", "u.id").where(`${t}.source_id`, "in", targets)),
|
|
48376
|
+
eb.exists(eb.selectFrom(t).select(`${t}.id`).whereRef(`${t}.unit_id`, "=", "u.id").where(`${t}.retraction_source_id`, "in", targets))
|
|
48377
|
+
]))).select(["u.id", "u.key", "u.lifecycle"]).orderBy("u.id").execute();
|
|
48378
|
+
const count = async (t, unitId) => Number((await trx.selectFrom(t).where("unit_id", "=", unitId).where((eb) => eb.or([eb("source_id", "in", targets), eb("retraction_source_id", "in", targets)])).select((eb) => eb.fn.countAll().as("n")).executeTakeFirstOrThrow()).n);
|
|
48379
|
+
const removed = new Map;
|
|
48380
|
+
for (const u of touched)
|
|
48381
|
+
removed.set(u.id, await count("unit_evidence", u.id) + await count("unit_adoption", u.id));
|
|
48382
|
+
const batch = (await trx.insertInto("forget_batch").values({ project_id: projectId2, at }).returning("id").executeTakeFirstOrThrow()).id;
|
|
48383
|
+
await trx.insertInto("source_forgotten").values(rows.map((r) => ({
|
|
48384
|
+
source_id: r.id,
|
|
48385
|
+
project_id: projectId2,
|
|
48386
|
+
artifact: r.artifact,
|
|
48387
|
+
kind: r.kind,
|
|
48388
|
+
external_id: r.external_id,
|
|
48389
|
+
revision: r.revision,
|
|
48390
|
+
content_hash: r.content_hash,
|
|
48391
|
+
batch_id: batch
|
|
48392
|
+
}))).execute();
|
|
48393
|
+
for (const t of ["unit_evidence", "unit_adoption"])
|
|
48394
|
+
await trx.deleteFrom(t).where("retraction_source_id", "in", targets).where("retracted_at", "is not", null).where("source_id", "not in", targets).execute();
|
|
48395
|
+
outcome.references = Number((await trx.deleteFrom("external_reference").where("owner_source_id", "in", targets).executeTakeFirst()).numDeletedRows);
|
|
48396
|
+
await trx.deleteFrom("source").where("id", "in", targets).execute();
|
|
48397
|
+
const reason2 = `sources ${targets.map((id) => `s${id}`).join(", ")} forgotten by the owner`;
|
|
48398
|
+
for (const u of touched) {
|
|
48399
|
+
let after = u.lifecycle;
|
|
48400
|
+
if (u.lifecycle === "active") {
|
|
48401
|
+
await trx.insertInto("unit_state").values({ unit_id: u.id, from_state: "active", to_state: "candidate", at, reason: reason2, forget_id: batch }).execute();
|
|
48402
|
+
after = "candidate";
|
|
48403
|
+
try {
|
|
48404
|
+
await trx.insertInto("unit_state").values({
|
|
48405
|
+
unit_id: u.id,
|
|
48406
|
+
from_state: "candidate",
|
|
48407
|
+
to_state: "active",
|
|
48408
|
+
at,
|
|
48409
|
+
reason: "support checked again after forgetting sources",
|
|
48410
|
+
forget_id: batch
|
|
48411
|
+
}).execute();
|
|
48412
|
+
after = "active";
|
|
48413
|
+
} catch (e) {
|
|
48414
|
+
if (!ACTIVATION.test(e.message))
|
|
48415
|
+
throw e;
|
|
48416
|
+
}
|
|
48417
|
+
}
|
|
48418
|
+
outcome.units.push({ key: u.key, before: u.lifecycle, after, removed: removed.get(u.id) ?? 0 });
|
|
48419
|
+
}
|
|
48420
|
+
return outcome;
|
|
48421
|
+
}
|
|
48422
|
+
async function previewForget(file2, projectId2, ids) {
|
|
48423
|
+
const db = openWriter("forget", file2);
|
|
48424
|
+
try {
|
|
48425
|
+
return await db.connection().execute(async (c) => {
|
|
48426
|
+
await sql`begin immediate`.execute(c);
|
|
48427
|
+
try {
|
|
48428
|
+
throw new Preview(await forgetIn(c, projectId2, ids, iso(Date.now())));
|
|
48429
|
+
} catch (e) {
|
|
48430
|
+
await sql`rollback`.execute(c).catch(() => {});
|
|
48431
|
+
if (e instanceof Preview)
|
|
48432
|
+
return e.outcome;
|
|
48433
|
+
throw e;
|
|
48434
|
+
}
|
|
48435
|
+
});
|
|
48436
|
+
} finally {
|
|
48437
|
+
await db.destroy();
|
|
48438
|
+
}
|
|
48439
|
+
}
|
|
48440
|
+
async function applyForget(file2, projectId2, ids, confirmed, signal) {
|
|
48441
|
+
const db = openWriter("forget", file2);
|
|
48442
|
+
try {
|
|
48443
|
+
return await db.connection().execute(async (c) => {
|
|
48444
|
+
await sql`pragma secure_delete = on`.execute(c);
|
|
48445
|
+
await sql`begin immediate`.execute(c);
|
|
48446
|
+
let outcome;
|
|
48447
|
+
try {
|
|
48448
|
+
outcome = await forgetIn(c, projectId2, ids, iso(Date.now()));
|
|
48449
|
+
if (JSON.stringify(outcome) !== JSON.stringify(confirmed))
|
|
48450
|
+
throw new Error("What these sources support changed after you confirmed. Nothing was forgotten; look at the preview again");
|
|
48451
|
+
if (signal?.aborted)
|
|
48452
|
+
throw new Error("The call was cancelled, so nothing was forgotten");
|
|
48453
|
+
await sql`commit`.execute(c);
|
|
48454
|
+
} catch (e) {
|
|
48455
|
+
await sql`rollback`.execute(c).catch(() => {});
|
|
48456
|
+
throw e;
|
|
48457
|
+
}
|
|
48458
|
+
try {
|
|
48459
|
+
await sql`insert into source_fts (source_fts) values ('optimize')`.execute(c);
|
|
48460
|
+
const checkpoint = await sql`pragma wal_checkpoint(TRUNCATE)`.execute(c);
|
|
48461
|
+
return { outcome, cleanup: checkpoint.rows[0]?.busy === 0 ? "done" : "incomplete" };
|
|
48462
|
+
} catch {
|
|
48463
|
+
return { outcome, cleanup: "incomplete" };
|
|
48464
|
+
}
|
|
48465
|
+
});
|
|
48466
|
+
} finally {
|
|
48467
|
+
await db.destroy();
|
|
48468
|
+
}
|
|
48469
|
+
}
|
|
48470
|
+
function forgetText(o) {
|
|
48471
|
+
const lines = o.sources.map((s) => `- s${s.id} ${s.kind} in ${inline(s.artifact).slice(0, 120)} (${plural2(s.bytes, "byte")})`);
|
|
48472
|
+
if (o.already.length)
|
|
48473
|
+
lines.push(`- already forgotten: ${o.already.map((id) => `s${id}`).join(", ")} (only the cleanup runs again)`);
|
|
48474
|
+
for (const u of o.units) {
|
|
48475
|
+
const change = u.before === u.after ? `stays ${u.after}` : `${u.before} → ${u.after}`;
|
|
48476
|
+
lines.push(`- record ${inline(u.key).slice(0, 120)}: ${change}, loses ${plural2(u.removed, "citation")}`);
|
|
48477
|
+
}
|
|
48478
|
+
if (o.references)
|
|
48479
|
+
lines.push(`- ${plural2(o.references, "unfetched reference")} the forgotten messages gave`);
|
|
48480
|
+
if (o.units.length)
|
|
48481
|
+
lines.push("Records keep their own text: if one repeats the forgotten words, they stay in it.");
|
|
48482
|
+
lines.push("Copies outside the database (capture's waiting and set-aside files, backups) are not touched.");
|
|
48483
|
+
return lines.join(`
|
|
48484
|
+
`);
|
|
48485
|
+
}
|
|
48486
|
+
|
|
48152
48487
|
// server/src/frame.ts
|
|
48153
48488
|
import crypto3 from "node:crypto";
|
|
48154
48489
|
function framed(body) {
|
|
@@ -48206,7 +48541,7 @@ var tool = (fn) => fn().then((t) => reply(t), (e) => reply(`Sphica: ${head(reaso
|
|
|
48206
48541
|
var server = new McpServer({ name: "sphica-record", version: versionAt(ROOT) ?? "unknown" }, {
|
|
48207
48542
|
capabilities: { experimental: { "codex/sandbox-state-meta": {} } },
|
|
48208
48543
|
instructions: [
|
|
48209
|
-
"Writes Sphica records for the trace, harvest, and
|
|
48544
|
+
"Writes Sphica records for the trace, harvest, glean, and forget Skills. Use these tools only while running one of those Skills.",
|
|
48210
48545
|
"Flow: begin (trace_begin, harvest_begin, or glean_begin) returns a run id; context shows what the run may cite; check the record; save it.",
|
|
48211
48546
|
"Always pass the repository root as cwd."
|
|
48212
48547
|
].join(`
|
|
@@ -48287,4 +48622,66 @@ server.registerTool("record_save", {
|
|
|
48287
48622
|
const p = await projectOf(a.cwd, extra._meta);
|
|
48288
48623
|
return saveText(conn(), a.run, p.projectId, p.root, a.record);
|
|
48289
48624
|
}));
|
|
48625
|
+
var SOURCES = exports_external.array(exports_external.string().regex(/^s[1-9][0-9]{0,15}$/)).min(1).max(50).describe("Sources to forget, as search and read show them (s12)");
|
|
48626
|
+
var idsOf = (refs) => refs.map((r) => Number(r.slice(1)));
|
|
48627
|
+
var ANSWER_MS = 10 * 60 * 1000;
|
|
48628
|
+
server.registerTool("forget_preview", {
|
|
48629
|
+
title: "What forgetting sources would do",
|
|
48630
|
+
description: "Shows which sources would be removed and which records would lose citations or leave active. Changes nothing. Never shows the text.",
|
|
48631
|
+
inputSchema: { sources: SOURCES, cwd: CWD },
|
|
48632
|
+
annotations: READ
|
|
48633
|
+
}, async (a, extra) => tool(async () => {
|
|
48634
|
+
const p = await projectOf(a.cwd, extra._meta);
|
|
48635
|
+
return forgetText(await previewForget(dbFile(), p.projectId, idsOf(a.sources)));
|
|
48636
|
+
}));
|
|
48637
|
+
server.registerTool("forget_apply", {
|
|
48638
|
+
title: "Forget sources",
|
|
48639
|
+
description: "Asks the owner in the host to confirm by typing the number of sources, then removes them, their index entries, and the bytes left in the file, and judges the records that cited them again. Nothing is removed without that answer.",
|
|
48640
|
+
inputSchema: { sources: SOURCES, cwd: CWD },
|
|
48641
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
48642
|
+
}, async (a, extra) => tool(async () => {
|
|
48643
|
+
const p = await projectOf(a.cwd, extra._meta);
|
|
48644
|
+
const ids = idsOf(a.sources);
|
|
48645
|
+
const seen = await previewForget(dbFile(), p.projectId, ids);
|
|
48646
|
+
const n = seen.sources.length;
|
|
48647
|
+
if (n > 0) {
|
|
48648
|
+
const refused = (why) => new Error(`${why}, so nothing was forgotten`);
|
|
48649
|
+
if (!server.server.getClientCapabilities()?.elicitation)
|
|
48650
|
+
throw refused("This host cannot ask you directly (run /sphica:forget in Claude Code)");
|
|
48651
|
+
let answer;
|
|
48652
|
+
try {
|
|
48653
|
+
answer = await server.server.elicitInput({
|
|
48654
|
+
mode: "form",
|
|
48655
|
+
message: `Forget ${plural2(n, "source")} for good?
|
|
48656
|
+
${forgetText(seen)}`,
|
|
48657
|
+
requestedSchema: {
|
|
48658
|
+
type: "object",
|
|
48659
|
+
properties: {
|
|
48660
|
+
confirm: {
|
|
48661
|
+
type: "string",
|
|
48662
|
+
title: "Number of sources",
|
|
48663
|
+
description: `Type ${n} to forget ${plural2(n, "source")}`
|
|
48664
|
+
}
|
|
48665
|
+
},
|
|
48666
|
+
required: ["confirm"]
|
|
48667
|
+
}
|
|
48668
|
+
}, { timeout: ANSWER_MS, signal: extra.signal });
|
|
48669
|
+
} catch (e) {
|
|
48670
|
+
throw refused(`The confirmation did not come back (${head(reason(e), 200)})`);
|
|
48671
|
+
}
|
|
48672
|
+
if (extra.signal.aborted)
|
|
48673
|
+
throw refused("The call was cancelled");
|
|
48674
|
+
if (answer.action !== "accept")
|
|
48675
|
+
throw refused(`You ${answer.action === "decline" ? "declined" : "cancelled"}`);
|
|
48676
|
+
if (String(answer.content?.confirm ?? "").trim() !== String(n))
|
|
48677
|
+
throw refused(`The number typed does not match ${n}`);
|
|
48678
|
+
}
|
|
48679
|
+
const done = await applyForget(dbFile(), p.projectId, ids, seen, extra.signal);
|
|
48680
|
+
return [
|
|
48681
|
+
n ? `Forgot ${plural2(n, "source")}.` : "Nothing new to forget.",
|
|
48682
|
+
forgetText(done.outcome),
|
|
48683
|
+
done.cleanup === "done" ? "The deleted text was cleared from the database file." : "Clearing the deleted text from the database file did not finish (another session may be reading it), so it may stay there until you run forget_apply with the same sources again."
|
|
48684
|
+
].join(`
|
|
48685
|
+
`);
|
|
48686
|
+
}));
|
|
48290
48687
|
await server.connect(new StdioServerTransport);
|