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/capture.js
CHANGED
|
@@ -10813,7 +10813,7 @@ import os from "node:os";
|
|
|
10813
10813
|
import path from "node:path";
|
|
10814
10814
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
10815
10815
|
var SCHEMA_GENERATION = 2;
|
|
10816
|
-
var SCHEMA_REVISION =
|
|
10816
|
+
var SCHEMA_REVISION = 2;
|
|
10817
10817
|
var sphicaHome = () => process.env.SPHICA_HOME || path.join(os.homedir(), ".sphica");
|
|
10818
10818
|
var dbFile = () => process.env.SPHICA_DB || path.join(sphicaHome(), "sphica.db");
|
|
10819
10819
|
function requireRuntime() {
|
|
@@ -10837,7 +10837,7 @@ function prepare(raw, check) {
|
|
|
10837
10837
|
const got = raw.prepare("pragma user_version").get()?.user_version;
|
|
10838
10838
|
if (got === SCHEMA_REVISION)
|
|
10839
10839
|
return;
|
|
10840
|
-
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "
|
|
10840
|
+
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."));
|
|
10841
10841
|
}
|
|
10842
10842
|
function generationOf(raw) {
|
|
10843
10843
|
const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
|
|
@@ -11234,22 +11234,54 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
11234
11234
|
return C2.SQLITE_DENY;
|
|
11235
11235
|
}
|
|
11236
11236
|
var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
|
|
11237
|
+
var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
|
|
11238
|
+
var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
|
|
11237
11239
|
function ingestAuthorizer(action, p1, p2) {
|
|
11238
11240
|
if (DDL().has(action))
|
|
11239
11241
|
return C2.SQLITE_DENY;
|
|
11240
|
-
if (writes(action) && p1 === "owner_identity")
|
|
11242
|
+
if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
|
|
11243
|
+
return C2.SQLITE_DENY;
|
|
11244
|
+
if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
|
|
11241
11245
|
return C2.SQLITE_DENY;
|
|
11242
11246
|
if (action === C2.SQLITE_PRAGMA)
|
|
11243
11247
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11244
11248
|
return C2.SQLITE_OK;
|
|
11245
11249
|
}
|
|
11250
|
+
var FORGET_WRITES = {
|
|
11251
|
+
[C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts"]),
|
|
11252
|
+
[C2.SQLITE_DELETE]: new Set([
|
|
11253
|
+
"source",
|
|
11254
|
+
"external_reference",
|
|
11255
|
+
"unit_evidence",
|
|
11256
|
+
"unit_adoption",
|
|
11257
|
+
"source_processing",
|
|
11258
|
+
"source_fts"
|
|
11259
|
+
])
|
|
11260
|
+
};
|
|
11261
|
+
var FORGET_UPDATES = {
|
|
11262
|
+
unit: new Set(["lifecycle", "revision"]),
|
|
11263
|
+
unit_state: new Set(["source_id"])
|
|
11264
|
+
};
|
|
11265
|
+
function forgetAuthorizer(action, p1, p2) {
|
|
11266
|
+
if (SHADOW.test(p1 ?? "") && writes(action))
|
|
11267
|
+
return C2.SQLITE_OK;
|
|
11268
|
+
if (action === C2.SQLITE_UPDATE)
|
|
11269
|
+
return FORGET_UPDATES[p1 ?? ""]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11270
|
+
if (action === C2.SQLITE_INSERT || action === C2.SQLITE_DELETE)
|
|
11271
|
+
return FORGET_WRITES[action]?.has(p1 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11272
|
+
if (action === C2.SQLITE_PRAGMA)
|
|
11273
|
+
return readsDataVersion(p1, p2) || p1 === "secure_delete" && ["on", "1", "true"].includes((p2 ?? "").toLowerCase()) || p1 === "wal_checkpoint" ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11274
|
+
if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
11275
|
+
return C2.SQLITE_OK;
|
|
11276
|
+
return C2.SQLITE_DENY;
|
|
11277
|
+
}
|
|
11246
11278
|
function connectWriter(role, file = dbFile(), create = false) {
|
|
11247
11279
|
requireRuntime();
|
|
11248
11280
|
if (!create)
|
|
11249
11281
|
requireFile(file);
|
|
11250
11282
|
const raw = new DatabaseSync2(file);
|
|
11251
11283
|
try {
|
|
11252
|
-
prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
|
|
11284
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
11253
11285
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
11254
11286
|
} catch (e) {
|
|
11255
11287
|
raw.close();
|
|
@@ -11257,6 +11289,8 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
11257
11289
|
}
|
|
11258
11290
|
if (role === "ingest")
|
|
11259
11291
|
raw.setAuthorizer(ingestAuthorizer);
|
|
11292
|
+
else if (role === "forget")
|
|
11293
|
+
raw.setAuthorizer(forgetAuthorizer);
|
|
11260
11294
|
else if (role === "capture") {
|
|
11261
11295
|
let own = false;
|
|
11262
11296
|
raw.setAuthorizer((action, p1, p2, _db, triggerOrView) => captureAuthorizer(own, action, p1, p2, triggerOrView));
|
|
@@ -11767,6 +11801,8 @@ function lock() {
|
|
|
11767
11801
|
return null;
|
|
11768
11802
|
}
|
|
11769
11803
|
var BATCH = 500;
|
|
11804
|
+
var FLUSH_BUDGET_MS = 30000;
|
|
11805
|
+
var TOOL_FLUSH_BUDGET_MS = 2000;
|
|
11770
11806
|
var ROWS = 1000;
|
|
11771
11807
|
var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
|
|
11772
11808
|
async function write(db, batch, projects) {
|
|
@@ -11842,98 +11878,135 @@ async function write(db, batch, projects) {
|
|
|
11842
11878
|
}
|
|
11843
11879
|
var REJECTED = new Set([18, 19, 20, 25]);
|
|
11844
11880
|
var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11881
|
+
var queued = (from) => {
|
|
11882
|
+
try {
|
|
11883
|
+
return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
|
|
11884
|
+
} catch {
|
|
11885
|
+
return [];
|
|
11886
|
+
}
|
|
11887
|
+
};
|
|
11888
|
+
async function sendBatch(db, names) {
|
|
11889
|
+
const held = unregisteredDir();
|
|
11890
|
+
const records = [];
|
|
11891
|
+
for (const { name, from } of names) {
|
|
11892
|
+
let r;
|
|
11893
|
+
try {
|
|
11894
|
+
r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
|
|
11895
|
+
} catch {
|
|
11896
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11897
|
+
fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
|
|
11898
|
+
continue;
|
|
11899
|
+
}
|
|
11900
|
+
if (r)
|
|
11901
|
+
records.push({ name, from, r });
|
|
11902
|
+
else
|
|
11903
|
+
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11904
|
+
}
|
|
11905
|
+
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 }]));
|
|
11906
|
+
const known = records.filter((x) => projects.has(x.r.project));
|
|
11907
|
+
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
11908
|
+
let sent = 0;
|
|
11909
|
+
const bad = [];
|
|
11851
11910
|
try {
|
|
11852
|
-
|
|
11853
|
-
|
|
11911
|
+
sent = await write(db, known.map((x) => x.r), projects);
|
|
11912
|
+
} catch (e) {
|
|
11913
|
+
if (!rejected(e))
|
|
11914
|
+
throw e;
|
|
11915
|
+
for (const x of known) {
|
|
11854
11916
|
try {
|
|
11855
|
-
|
|
11856
|
-
} catch {
|
|
11857
|
-
|
|
11917
|
+
sent += await write(db, [x.r], projects);
|
|
11918
|
+
} catch (e2) {
|
|
11919
|
+
if (!rejected(e2))
|
|
11920
|
+
throw e2;
|
|
11921
|
+
bad.push(x);
|
|
11858
11922
|
}
|
|
11859
|
-
}
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
const
|
|
11864
|
-
for (const { name, from } of names) {
|
|
11865
|
-
let r;
|
|
11923
|
+
}
|
|
11924
|
+
}
|
|
11925
|
+
if (bad.length) {
|
|
11926
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11927
|
+
for (const x of bad) {
|
|
11866
11928
|
try {
|
|
11867
|
-
|
|
11868
|
-
} catch {
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
continue;
|
|
11872
|
-
}
|
|
11873
|
-
if (r)
|
|
11874
|
-
records.push({ name, from, r });
|
|
11875
|
-
else
|
|
11876
|
-
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11877
|
-
}
|
|
11878
|
-
const db = openWriter("capture", file);
|
|
11879
|
-
client = db;
|
|
11880
|
-
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 }]));
|
|
11881
|
-
const known = records.filter((x) => projects.has(x.r.project));
|
|
11882
|
-
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
11883
|
-
let sent = 0;
|
|
11884
|
-
const bad = [];
|
|
11885
|
-
try {
|
|
11886
|
-
sent = await write(db, known.map((x) => x.r), projects);
|
|
11887
|
-
} catch (e) {
|
|
11888
|
-
if (!rejected(e))
|
|
11889
|
-
throw e;
|
|
11890
|
-
for (const x of known) {
|
|
11891
|
-
try {
|
|
11892
|
-
sent += await write(db, [x.r], projects);
|
|
11893
|
-
} catch (e2) {
|
|
11894
|
-
if (!rejected(e2))
|
|
11895
|
-
throw e2;
|
|
11896
|
-
bad.push(x);
|
|
11897
|
-
}
|
|
11929
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
|
|
11930
|
+
} catch (e) {
|
|
11931
|
+
if (e.code !== "ENOENT")
|
|
11932
|
+
throw e;
|
|
11898
11933
|
}
|
|
11899
11934
|
}
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11935
|
+
}
|
|
11936
|
+
if (strayed.length) {
|
|
11937
|
+
fs4.mkdirSync(held, { recursive: true, mode: 448 });
|
|
11938
|
+
for (const x of strayed) {
|
|
11939
|
+
if (x.from === held)
|
|
11940
|
+
continue;
|
|
11941
|
+
try {
|
|
11942
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
|
|
11943
|
+
} catch (e) {
|
|
11944
|
+
if (e.code !== "ENOENT")
|
|
11945
|
+
throw e;
|
|
11909
11946
|
}
|
|
11910
11947
|
}
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11948
|
+
}
|
|
11949
|
+
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
11950
|
+
for (const x of records)
|
|
11951
|
+
if (!moved.has(x.name))
|
|
11952
|
+
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11953
|
+
return { sent, rejected: bad.length };
|
|
11954
|
+
}
|
|
11955
|
+
async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
11956
|
+
const deadline = Date.now() + budgetMs;
|
|
11957
|
+
const dir = spoolDir();
|
|
11958
|
+
const held = unregisteredDir();
|
|
11959
|
+
const total = { sent: 0, deferred: 0, rejected: 0 };
|
|
11960
|
+
let batches = 0;
|
|
11961
|
+
let queueBatches = 0;
|
|
11962
|
+
for (let first = true;; first = false) {
|
|
11963
|
+
let unlock = lock();
|
|
11964
|
+
while (!unlock && Date.now() < deadline) {
|
|
11965
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
|
|
11966
|
+
if (Date.now() < deadline)
|
|
11967
|
+
unlock = lock();
|
|
11968
|
+
}
|
|
11969
|
+
if (!unlock)
|
|
11970
|
+
return first ? { ...total, busy: true } : total;
|
|
11971
|
+
let client = null;
|
|
11972
|
+
const send = async (names) => {
|
|
11973
|
+
client ??= openWriter("capture", file);
|
|
11974
|
+
const r = await sendBatch(client, names);
|
|
11975
|
+
total.sent += r.sent;
|
|
11976
|
+
total.rejected += r.rejected;
|
|
11977
|
+
batches++;
|
|
11978
|
+
};
|
|
11979
|
+
const late = () => batches > 0 && Date.now() >= deadline;
|
|
11980
|
+
const sentBefore = batches;
|
|
11981
|
+
try {
|
|
11982
|
+
if (first) {
|
|
11983
|
+
prune(held);
|
|
11984
|
+
for (const part of chunks(queued(held), BATCH)) {
|
|
11985
|
+
if (late())
|
|
11986
|
+
break;
|
|
11987
|
+
await send(part);
|
|
11921
11988
|
}
|
|
11922
11989
|
}
|
|
11990
|
+
while (queueBatches === 0 || !late()) {
|
|
11991
|
+
const part = queued(dir).slice(0, BATCH);
|
|
11992
|
+
if (part.length === 0)
|
|
11993
|
+
break;
|
|
11994
|
+
await send(part);
|
|
11995
|
+
queueBatches++;
|
|
11996
|
+
}
|
|
11997
|
+
prune(held);
|
|
11998
|
+
total.deferred = queued(held).length;
|
|
11999
|
+
if (batches > sentBefore)
|
|
12000
|
+
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
|
|
12001
|
+
} catch (e) {
|
|
12002
|
+
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
12003
|
+
throw e;
|
|
12004
|
+
} finally {
|
|
12005
|
+
await client?.destroy().catch(() => {});
|
|
12006
|
+
unlock();
|
|
11923
12007
|
}
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
if (!moved.has(x.name))
|
|
11927
|
-
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11928
|
-
prune(held);
|
|
11929
|
-
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
11930
|
-
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
11931
|
-
} catch (e) {
|
|
11932
|
-
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
11933
|
-
throw e;
|
|
11934
|
-
} finally {
|
|
11935
|
-
await client?.destroy().catch(() => {});
|
|
11936
|
-
unlock();
|
|
12008
|
+
if (Date.now() >= deadline || queued(dir).length === 0)
|
|
12009
|
+
return total;
|
|
11937
12010
|
}
|
|
11938
12011
|
}
|
|
11939
12012
|
async function readInput(stream) {
|
|
@@ -11963,6 +12036,7 @@ if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
|
11963
12036
|
}
|
|
11964
12037
|
export {
|
|
11965
12038
|
MAX_MESSAGE,
|
|
12039
|
+
TOOL_FLUSH_BUDGET_MS,
|
|
11966
12040
|
answersOf,
|
|
11967
12041
|
branchOf,
|
|
11968
12042
|
captureNotice,
|