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/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 = 1;
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 ? "Move it aside (it is left unchanged), then run `sphica init`." : "Update sphica."));
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
- async function flush(file = dbFile()) {
11846
- const unlock = lock();
11847
- if (!unlock)
11848
- return { sent: 0, deferred: 0, rejected: 0, busy: true };
11849
- const dir = spoolDir();
11850
- let client = null;
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
- const held = unregisteredDir();
11853
- const list = (from) => {
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
- return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
11856
- } catch {
11857
- return [];
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
- const names = [...list(dir), ...list(held)].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).slice(0, BATCH);
11861
- if (names.length === 0)
11862
- return { sent: 0, deferred: 0, rejected: 0 };
11863
- const records = [];
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
- r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
11868
- } catch {
11869
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11870
- fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
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
- if (bad.length) {
11901
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11902
- for (const x of bad) {
11903
- try {
11904
- fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
11905
- } catch (e) {
11906
- if (e.code !== "ENOENT")
11907
- throw e;
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
- if (strayed.length) {
11912
- fs4.mkdirSync(held, { recursive: true, mode: 448 });
11913
- for (const x of strayed) {
11914
- if (x.from === held)
11915
- continue;
11916
- try {
11917
- fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
11918
- } catch (e) {
11919
- if (e.code !== "ENOENT")
11920
- throw e;
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
- const moved = new Set([...bad, ...strayed].map((x) => x.name));
11925
- for (const x of records)
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,