sphica 0.5.7 → 0.6.1
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 +159 -85
- package/dist/cli.js +191 -88
- package/dist/deliver.js +157 -85
- package/dist/mcp-record.js +399 -102
- package/dist/mcp.js +2 -2
- package/package.json +1 -1
- package/skills/forget/SKILL.md +53 -0
- package/skills/forget/agents/openai.yaml +2 -0
package/dist/deliver.js
CHANGED
|
@@ -11086,7 +11086,7 @@ import os from "node:os";
|
|
|
11086
11086
|
import path2 from "node:path";
|
|
11087
11087
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
11088
11088
|
var SCHEMA_GENERATION = 2;
|
|
11089
|
-
var SCHEMA_REVISION =
|
|
11089
|
+
var SCHEMA_REVISION = 2;
|
|
11090
11090
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
11091
11091
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
11092
11092
|
function requireRuntime() {
|
|
@@ -11110,7 +11110,7 @@ function prepare(raw, check) {
|
|
|
11110
11110
|
const got = raw.prepare("pragma user_version").get()?.user_version;
|
|
11111
11111
|
if (got === SCHEMA_REVISION)
|
|
11112
11112
|
return;
|
|
11113
|
-
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "
|
|
11113
|
+
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."));
|
|
11114
11114
|
}
|
|
11115
11115
|
function generationOf(raw) {
|
|
11116
11116
|
const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
|
|
@@ -11282,22 +11282,54 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
11282
11282
|
return C2.SQLITE_DENY;
|
|
11283
11283
|
}
|
|
11284
11284
|
var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
|
|
11285
|
+
var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
|
|
11286
|
+
var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
|
|
11285
11287
|
function ingestAuthorizer(action, p1, p2) {
|
|
11286
11288
|
if (DDL().has(action))
|
|
11287
11289
|
return C2.SQLITE_DENY;
|
|
11288
|
-
if (writes(action) && p1 === "owner_identity")
|
|
11290
|
+
if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
|
|
11291
|
+
return C2.SQLITE_DENY;
|
|
11292
|
+
if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
|
|
11289
11293
|
return C2.SQLITE_DENY;
|
|
11290
11294
|
if (action === C2.SQLITE_PRAGMA)
|
|
11291
11295
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11292
11296
|
return C2.SQLITE_OK;
|
|
11293
11297
|
}
|
|
11298
|
+
var FORGET_WRITES = {
|
|
11299
|
+
[C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts"]),
|
|
11300
|
+
[C2.SQLITE_DELETE]: new Set([
|
|
11301
|
+
"source",
|
|
11302
|
+
"external_reference",
|
|
11303
|
+
"unit_evidence",
|
|
11304
|
+
"unit_adoption",
|
|
11305
|
+
"source_processing",
|
|
11306
|
+
"source_fts"
|
|
11307
|
+
])
|
|
11308
|
+
};
|
|
11309
|
+
var FORGET_UPDATES = {
|
|
11310
|
+
unit: new Set(["lifecycle", "revision"]),
|
|
11311
|
+
unit_state: new Set(["source_id"])
|
|
11312
|
+
};
|
|
11313
|
+
function forgetAuthorizer(action, p1, p2) {
|
|
11314
|
+
if (SHADOW.test(p1 ?? "") && writes(action))
|
|
11315
|
+
return C2.SQLITE_OK;
|
|
11316
|
+
if (action === C2.SQLITE_UPDATE)
|
|
11317
|
+
return FORGET_UPDATES[p1 ?? ""]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11318
|
+
if (action === C2.SQLITE_INSERT || action === C2.SQLITE_DELETE)
|
|
11319
|
+
return FORGET_WRITES[action]?.has(p1 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11320
|
+
if (action === C2.SQLITE_PRAGMA)
|
|
11321
|
+
return readsDataVersion(p1, p2) || p1 === "secure_delete" && ["on", "1", "true"].includes((p2 ?? "").toLowerCase()) || p1 === "wal_checkpoint" ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11322
|
+
if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
11323
|
+
return C2.SQLITE_OK;
|
|
11324
|
+
return C2.SQLITE_DENY;
|
|
11325
|
+
}
|
|
11294
11326
|
function connectWriter(role, file = dbFile(), create = false) {
|
|
11295
11327
|
requireRuntime();
|
|
11296
11328
|
if (!create)
|
|
11297
11329
|
requireFile(file);
|
|
11298
11330
|
const raw = new DatabaseSync2(file);
|
|
11299
11331
|
try {
|
|
11300
|
-
prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
|
|
11332
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
11301
11333
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
11302
11334
|
} catch (e) {
|
|
11303
11335
|
raw.close();
|
|
@@ -11305,6 +11337,8 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
11305
11337
|
}
|
|
11306
11338
|
if (role === "ingest")
|
|
11307
11339
|
raw.setAuthorizer(ingestAuthorizer);
|
|
11340
|
+
else if (role === "forget")
|
|
11341
|
+
raw.setAuthorizer(forgetAuthorizer);
|
|
11308
11342
|
else if (role === "capture") {
|
|
11309
11343
|
let own = false;
|
|
11310
11344
|
raw.setAuthorizer((action, p1, p2, _db, triggerOrView) => captureAuthorizer(own, action, p1, p2, triggerOrView));
|
|
@@ -11813,6 +11847,7 @@ function lock() {
|
|
|
11813
11847
|
return null;
|
|
11814
11848
|
}
|
|
11815
11849
|
var BATCH = 500;
|
|
11850
|
+
var FLUSH_BUDGET_MS = 30000;
|
|
11816
11851
|
var ROWS = 1000;
|
|
11817
11852
|
var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
|
|
11818
11853
|
async function write(db, batch, projects) {
|
|
@@ -11888,98 +11923,135 @@ async function write(db, batch, projects) {
|
|
|
11888
11923
|
}
|
|
11889
11924
|
var REJECTED = new Set([18, 19, 20, 25]);
|
|
11890
11925
|
var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11926
|
+
var queued = (from) => {
|
|
11927
|
+
try {
|
|
11928
|
+
return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
|
|
11929
|
+
} catch {
|
|
11930
|
+
return [];
|
|
11931
|
+
}
|
|
11932
|
+
};
|
|
11933
|
+
async function sendBatch(db, names) {
|
|
11934
|
+
const held = unregisteredDir();
|
|
11935
|
+
const records = [];
|
|
11936
|
+
for (const { name, from } of names) {
|
|
11937
|
+
let r;
|
|
11938
|
+
try {
|
|
11939
|
+
r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
|
|
11940
|
+
} catch {
|
|
11941
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11942
|
+
fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
|
|
11943
|
+
continue;
|
|
11944
|
+
}
|
|
11945
|
+
if (r)
|
|
11946
|
+
records.push({ name, from, r });
|
|
11947
|
+
else
|
|
11948
|
+
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11949
|
+
}
|
|
11950
|
+
const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
|
|
11951
|
+
const known = records.filter((x) => projects.has(x.r.project));
|
|
11952
|
+
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
11953
|
+
let sent = 0;
|
|
11954
|
+
const bad = [];
|
|
11897
11955
|
try {
|
|
11898
|
-
|
|
11899
|
-
|
|
11956
|
+
sent = await write(db, known.map((x) => x.r), projects);
|
|
11957
|
+
} catch (e) {
|
|
11958
|
+
if (!rejected(e))
|
|
11959
|
+
throw e;
|
|
11960
|
+
for (const x of known) {
|
|
11900
11961
|
try {
|
|
11901
|
-
|
|
11902
|
-
} catch {
|
|
11903
|
-
|
|
11962
|
+
sent += await write(db, [x.r], projects);
|
|
11963
|
+
} catch (e2) {
|
|
11964
|
+
if (!rejected(e2))
|
|
11965
|
+
throw e2;
|
|
11966
|
+
bad.push(x);
|
|
11904
11967
|
}
|
|
11905
|
-
}
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
const
|
|
11910
|
-
for (const { name, from } of names) {
|
|
11911
|
-
let r;
|
|
11968
|
+
}
|
|
11969
|
+
}
|
|
11970
|
+
if (bad.length) {
|
|
11971
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11972
|
+
for (const x of bad) {
|
|
11912
11973
|
try {
|
|
11913
|
-
|
|
11914
|
-
} catch {
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
continue;
|
|
11918
|
-
}
|
|
11919
|
-
if (r)
|
|
11920
|
-
records.push({ name, from, r });
|
|
11921
|
-
else
|
|
11922
|
-
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11923
|
-
}
|
|
11924
|
-
const db = openWriter("capture", file);
|
|
11925
|
-
client = db;
|
|
11926
|
-
const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
|
|
11927
|
-
const known = records.filter((x) => projects.has(x.r.project));
|
|
11928
|
-
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
11929
|
-
let sent = 0;
|
|
11930
|
-
const bad = [];
|
|
11931
|
-
try {
|
|
11932
|
-
sent = await write(db, known.map((x) => x.r), projects);
|
|
11933
|
-
} catch (e) {
|
|
11934
|
-
if (!rejected(e))
|
|
11935
|
-
throw e;
|
|
11936
|
-
for (const x of known) {
|
|
11937
|
-
try {
|
|
11938
|
-
sent += await write(db, [x.r], projects);
|
|
11939
|
-
} catch (e2) {
|
|
11940
|
-
if (!rejected(e2))
|
|
11941
|
-
throw e2;
|
|
11942
|
-
bad.push(x);
|
|
11943
|
-
}
|
|
11974
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
|
|
11975
|
+
} catch (e) {
|
|
11976
|
+
if (e.code !== "ENOENT")
|
|
11977
|
+
throw e;
|
|
11944
11978
|
}
|
|
11945
11979
|
}
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11980
|
+
}
|
|
11981
|
+
if (strayed.length) {
|
|
11982
|
+
fs4.mkdirSync(held, { recursive: true, mode: 448 });
|
|
11983
|
+
for (const x of strayed) {
|
|
11984
|
+
if (x.from === held)
|
|
11985
|
+
continue;
|
|
11986
|
+
try {
|
|
11987
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
|
|
11988
|
+
} catch (e) {
|
|
11989
|
+
if (e.code !== "ENOENT")
|
|
11990
|
+
throw e;
|
|
11955
11991
|
}
|
|
11956
11992
|
}
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11993
|
+
}
|
|
11994
|
+
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
11995
|
+
for (const x of records)
|
|
11996
|
+
if (!moved.has(x.name))
|
|
11997
|
+
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11998
|
+
return { sent, rejected: bad.length };
|
|
11999
|
+
}
|
|
12000
|
+
async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
12001
|
+
const deadline = Date.now() + budgetMs;
|
|
12002
|
+
const dir = spoolDir();
|
|
12003
|
+
const held = unregisteredDir();
|
|
12004
|
+
const total = { sent: 0, deferred: 0, rejected: 0 };
|
|
12005
|
+
let batches = 0;
|
|
12006
|
+
let queueBatches = 0;
|
|
12007
|
+
for (let first = true;; first = false) {
|
|
12008
|
+
let unlock = lock();
|
|
12009
|
+
while (!unlock && Date.now() < deadline) {
|
|
12010
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
|
|
12011
|
+
if (Date.now() < deadline)
|
|
12012
|
+
unlock = lock();
|
|
12013
|
+
}
|
|
12014
|
+
if (!unlock)
|
|
12015
|
+
return first ? { ...total, busy: true } : total;
|
|
12016
|
+
let client = null;
|
|
12017
|
+
const send = async (names) => {
|
|
12018
|
+
client ??= openWriter("capture", file);
|
|
12019
|
+
const r = await sendBatch(client, names);
|
|
12020
|
+
total.sent += r.sent;
|
|
12021
|
+
total.rejected += r.rejected;
|
|
12022
|
+
batches++;
|
|
12023
|
+
};
|
|
12024
|
+
const late = () => batches > 0 && Date.now() >= deadline;
|
|
12025
|
+
const sentBefore = batches;
|
|
12026
|
+
try {
|
|
12027
|
+
if (first) {
|
|
12028
|
+
prune(held);
|
|
12029
|
+
for (const part of chunks(queued(held), BATCH)) {
|
|
12030
|
+
if (late())
|
|
12031
|
+
break;
|
|
12032
|
+
await send(part);
|
|
11967
12033
|
}
|
|
11968
12034
|
}
|
|
12035
|
+
while (queueBatches === 0 || !late()) {
|
|
12036
|
+
const part = queued(dir).slice(0, BATCH);
|
|
12037
|
+
if (part.length === 0)
|
|
12038
|
+
break;
|
|
12039
|
+
await send(part);
|
|
12040
|
+
queueBatches++;
|
|
12041
|
+
}
|
|
12042
|
+
prune(held);
|
|
12043
|
+
total.deferred = queued(held).length;
|
|
12044
|
+
if (batches > sentBefore)
|
|
12045
|
+
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
|
|
12046
|
+
} catch (e) {
|
|
12047
|
+
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
12048
|
+
throw e;
|
|
12049
|
+
} finally {
|
|
12050
|
+
await client?.destroy().catch(() => {});
|
|
12051
|
+
unlock();
|
|
11969
12052
|
}
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
if (!moved.has(x.name))
|
|
11973
|
-
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11974
|
-
prune(held);
|
|
11975
|
-
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
11976
|
-
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
11977
|
-
} catch (e) {
|
|
11978
|
-
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
11979
|
-
throw e;
|
|
11980
|
-
} finally {
|
|
11981
|
-
await client?.destroy().catch(() => {});
|
|
11982
|
-
unlock();
|
|
12053
|
+
if (Date.now() >= deadline || queued(dir).length === 0)
|
|
12054
|
+
return total;
|
|
11983
12055
|
}
|
|
11984
12056
|
}
|
|
11985
12057
|
async function readInput(stream) {
|