sphica 0.6.13 → 0.6.15

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
@@ -11109,7 +11109,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
11109
11109
 
11110
11110
  // server/src/sqlite.ts
11111
11111
  var SCHEMA_GENERATION = 2;
11112
- var SCHEMA_REVISION = 4;
11112
+ var SCHEMA_REVISION = 5;
11113
11113
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
11114
11114
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
11115
11115
  function requireRuntime() {
@@ -11287,24 +11287,92 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
11287
11287
  return C2.SQLITE_DENY;
11288
11288
  }
11289
11289
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
11290
- var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
11291
- var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
11292
- function ingestAuthorizer(action, p1, p2) {
11290
+ var INGEST_INSERTS = new Set([
11291
+ "project",
11292
+ "ingest_source",
11293
+ "extraction_run",
11294
+ "source_processing",
11295
+ "unit",
11296
+ "unit_option",
11297
+ "unit_evidence",
11298
+ "unit_adoption",
11299
+ "unit_link",
11300
+ "unit_state",
11301
+ "unit_anchor",
11302
+ "unit_alias",
11303
+ "field_def",
11304
+ "unit_field",
11305
+ "work",
11306
+ "artifact_link"
11307
+ ]);
11308
+ var RETRACTION = [
11309
+ "retracted_at",
11310
+ "retraction_reason",
11311
+ "retraction_source_id",
11312
+ "retraction_span_start",
11313
+ "retraction_span_end"
11314
+ ];
11315
+ var INGEST_UPDATES = {
11316
+ extraction_run: new Set(["status", "finished_at"]),
11317
+ unit_anchor: new Set(["retired_at", "replaced_by"]),
11318
+ unit_link: new Set(["resolved_at", "resolution"]),
11319
+ unit_evidence: new Set(RETRACTION),
11320
+ unit_adoption: new Set(RETRACTION),
11321
+ work: new Set(["title", "goal", "current", "next", "status", "branch", "run_id", "updated_at"])
11322
+ };
11323
+ var INGEST_DELETES = new Set(["artifact_link"]);
11324
+ var INGEST_TRIGGER_WRITES = {
11325
+ ingest_source_insert: ["insert source"],
11326
+ source_fts_ai: ["insert source_fts"],
11327
+ source_fts_ad: ["delete source_fts"],
11328
+ unit_state_apply: ["update unit"],
11329
+ unit_state_restore: ["insert unit_state"],
11330
+ ...Object.fromEntries([
11331
+ "evidence_i",
11332
+ "evidence_u",
11333
+ "evidence_d",
11334
+ "adoption_i",
11335
+ "adoption_u",
11336
+ "adoption_d",
11337
+ "link_i",
11338
+ "link_u",
11339
+ "anchor_i",
11340
+ "anchor_u",
11341
+ "alias_i",
11342
+ "field_i",
11343
+ "field_d"
11344
+ ].map((t) => [`unit_rev_${t}`, ["update unit"]])),
11345
+ unit_fts_ai: ["insert unit_fts"],
11346
+ unit_fts_ad: ["delete unit_fts"],
11347
+ ...Object.fromEntries(["option_i", "anchor_i", "anchor_u", "anchor_d", "alias_i", "alias_d", "field_i", "field_d"].map((t) => [
11348
+ `unit_fts_${t}`,
11349
+ ["delete unit_fts", "insert unit_fts"]
11350
+ ]))
11351
+ };
11352
+ var opOf = (action) => action === C2.SQLITE_INSERT ? "insert" : action === C2.SQLITE_UPDATE ? "update" : action === C2.SQLITE_DELETE ? "delete" : undefined;
11353
+ function ingestAuthorizer(action, p1, p2, trigger) {
11293
11354
  if (DDL().has(action))
11294
11355
  return C2.SQLITE_DENY;
11295
- if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
11296
- return C2.SQLITE_DENY;
11297
- if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
11298
- return C2.SQLITE_DENY;
11356
+ const op = opOf(action);
11357
+ if (op) {
11358
+ const table = p1 ?? "";
11359
+ if (SHADOW.test(table))
11360
+ return C2.SQLITE_OK;
11361
+ if (trigger !== null)
11362
+ return INGEST_TRIGGER_WRITES[trigger]?.includes(`${op} ${table}`) ? C2.SQLITE_OK : C2.SQLITE_DENY;
11363
+ const allowed = op === "insert" ? INGEST_INSERTS.has(table) : op === "update" ? INGEST_UPDATES[table]?.has(p2 ?? "") : INGEST_DELETES.has(table);
11364
+ return allowed ? C2.SQLITE_OK : C2.SQLITE_DENY;
11365
+ }
11299
11366
  if (action === C2.SQLITE_PRAGMA)
11300
11367
  return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
11301
- return C2.SQLITE_OK;
11368
+ if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_RECURSIVE || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
11369
+ return C2.SQLITE_OK;
11370
+ return C2.SQLITE_DENY;
11302
11371
  }
11303
11372
  var FORGET_WRITES = {
11304
11373
  [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts", "unit_fts"]),
11305
11374
  [C2.SQLITE_DELETE]: new Set([
11306
11375
  "source",
11307
- "external_reference",
11308
11376
  "unit_evidence",
11309
11377
  "unit_adoption",
11310
11378
  "source_processing",
@@ -11344,7 +11412,7 @@ function connectWriter(role, file = dbFile(), create = false) {
11344
11412
  throw e;
11345
11413
  }
11346
11414
  if (role === "ingest")
11347
- raw.setAuthorizer(ingestAuthorizer);
11415
+ raw.setAuthorizer((action, p1, p2, _db, trigger) => ingestAuthorizer(action, p1, p2, trigger));
11348
11416
  else if (role === "forget")
11349
11417
  raw.setAuthorizer(forgetAuthorizer);
11350
11418
  else if (role === "capture") {
@@ -11451,7 +11519,7 @@ function identify(dir) {
11451
11519
  function relativeTo(root, file, cwd = root) {
11452
11520
  const abs = path4.resolve(cwd, file);
11453
11521
  const rel = path4.relative(root, abs);
11454
- if (!rel || leaves(rel))
11522
+ if (!rel || leaves(rel) || /\p{Cc}/u.test(rel))
11455
11523
  return null;
11456
11524
  return rel.split(path4.sep).join("/");
11457
11525
  }
@@ -11483,7 +11551,7 @@ var git2 = (root, args) => {
11483
11551
  return null;
11484
11552
  }
11485
11553
  };
11486
- var usable = (p) => p !== "" && !p.includes("\\");
11554
+ var usable = (p) => p !== "" && !p.includes("\\") && !/\p{Cc}/u.test(p);
11487
11555
  function snapshot(root) {
11488
11556
  const out = git2(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
11489
11557
  if (out === null)
package/dist/cli.js CHANGED
@@ -3625,7 +3625,7 @@ ${indent}${caution(note)}` : ""}`);
3625
3625
 
3626
3626
  // server/src/sqlite.ts
3627
3627
  var SCHEMA_GENERATION = 2;
3628
- var SCHEMA_REVISION = 4;
3628
+ var SCHEMA_REVISION = 5;
3629
3629
  var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os2.homedir(), ".sphica");
3630
3630
  var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
3631
3631
  function requireRuntime() {
@@ -14754,24 +14754,92 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
14754
14754
  return C2.SQLITE_DENY;
14755
14755
  }
14756
14756
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
14757
- var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
14758
- var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
14759
- function ingestAuthorizer(action, p1, p2) {
14757
+ var INGEST_INSERTS = new Set([
14758
+ "project",
14759
+ "ingest_source",
14760
+ "extraction_run",
14761
+ "source_processing",
14762
+ "unit",
14763
+ "unit_option",
14764
+ "unit_evidence",
14765
+ "unit_adoption",
14766
+ "unit_link",
14767
+ "unit_state",
14768
+ "unit_anchor",
14769
+ "unit_alias",
14770
+ "field_def",
14771
+ "unit_field",
14772
+ "work",
14773
+ "artifact_link"
14774
+ ]);
14775
+ var RETRACTION = [
14776
+ "retracted_at",
14777
+ "retraction_reason",
14778
+ "retraction_source_id",
14779
+ "retraction_span_start",
14780
+ "retraction_span_end"
14781
+ ];
14782
+ var INGEST_UPDATES = {
14783
+ extraction_run: new Set(["status", "finished_at"]),
14784
+ unit_anchor: new Set(["retired_at", "replaced_by"]),
14785
+ unit_link: new Set(["resolved_at", "resolution"]),
14786
+ unit_evidence: new Set(RETRACTION),
14787
+ unit_adoption: new Set(RETRACTION),
14788
+ work: new Set(["title", "goal", "current", "next", "status", "branch", "run_id", "updated_at"])
14789
+ };
14790
+ var INGEST_DELETES = new Set(["artifact_link"]);
14791
+ var INGEST_TRIGGER_WRITES = {
14792
+ ingest_source_insert: ["insert source"],
14793
+ source_fts_ai: ["insert source_fts"],
14794
+ source_fts_ad: ["delete source_fts"],
14795
+ unit_state_apply: ["update unit"],
14796
+ unit_state_restore: ["insert unit_state"],
14797
+ ...Object.fromEntries([
14798
+ "evidence_i",
14799
+ "evidence_u",
14800
+ "evidence_d",
14801
+ "adoption_i",
14802
+ "adoption_u",
14803
+ "adoption_d",
14804
+ "link_i",
14805
+ "link_u",
14806
+ "anchor_i",
14807
+ "anchor_u",
14808
+ "alias_i",
14809
+ "field_i",
14810
+ "field_d"
14811
+ ].map((t2) => [`unit_rev_${t2}`, ["update unit"]])),
14812
+ unit_fts_ai: ["insert unit_fts"],
14813
+ unit_fts_ad: ["delete unit_fts"],
14814
+ ...Object.fromEntries(["option_i", "anchor_i", "anchor_u", "anchor_d", "alias_i", "alias_d", "field_i", "field_d"].map((t2) => [
14815
+ `unit_fts_${t2}`,
14816
+ ["delete unit_fts", "insert unit_fts"]
14817
+ ]))
14818
+ };
14819
+ var opOf = (action) => action === C2.SQLITE_INSERT ? "insert" : action === C2.SQLITE_UPDATE ? "update" : action === C2.SQLITE_DELETE ? "delete" : undefined;
14820
+ function ingestAuthorizer(action, p1, p2, trigger) {
14760
14821
  if (DDL().has(action))
14761
14822
  return C2.SQLITE_DENY;
14762
- if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
14763
- return C2.SQLITE_DENY;
14764
- if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
14765
- return C2.SQLITE_DENY;
14823
+ const op = opOf(action);
14824
+ if (op) {
14825
+ const table = p1 ?? "";
14826
+ if (SHADOW.test(table))
14827
+ return C2.SQLITE_OK;
14828
+ if (trigger !== null)
14829
+ return INGEST_TRIGGER_WRITES[trigger]?.includes(`${op} ${table}`) ? C2.SQLITE_OK : C2.SQLITE_DENY;
14830
+ const allowed = op === "insert" ? INGEST_INSERTS.has(table) : op === "update" ? INGEST_UPDATES[table]?.has(p2 ?? "") : INGEST_DELETES.has(table);
14831
+ return allowed ? C2.SQLITE_OK : C2.SQLITE_DENY;
14832
+ }
14766
14833
  if (action === C2.SQLITE_PRAGMA)
14767
14834
  return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
14768
- return C2.SQLITE_OK;
14835
+ if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_RECURSIVE || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
14836
+ return C2.SQLITE_OK;
14837
+ return C2.SQLITE_DENY;
14769
14838
  }
14770
14839
  var FORGET_WRITES = {
14771
14840
  [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts", "unit_fts"]),
14772
14841
  [C2.SQLITE_DELETE]: new Set([
14773
14842
  "source",
14774
- "external_reference",
14775
14843
  "unit_evidence",
14776
14844
  "unit_adoption",
14777
14845
  "source_processing",
@@ -14811,7 +14879,7 @@ function connectWriter(role, file = dbFile(), create = false) {
14811
14879
  throw e;
14812
14880
  }
14813
14881
  if (role === "ingest")
14814
- raw.setAuthorizer(ingestAuthorizer);
14882
+ raw.setAuthorizer((action, p1, p2, _db, trigger) => ingestAuthorizer(action, p1, p2, trigger));
14815
14883
  else if (role === "forget")
14816
14884
  raw.setAuthorizer(forgetAuthorizer);
14817
14885
  else if (role === "capture") {
@@ -14841,6 +14909,9 @@ var say = (text) => console.log(indent(text));
14841
14909
  var SCHEMA = () => path5.join(dbDir(), "schema.sql");
14842
14910
  var MIGRATIONS = () => path5.join(dbDir(), "migrations");
14843
14911
  var MIGRATION = (dir, revision) => path5.join(dir, `${String(revision).padStart(4, "0")}.sql`);
14912
+ var CHECK = (dir, revision) => path5.join(dir, `${String(revision).padStart(4, "0")}.check.sql`);
14913
+ var STOP2 = "sphica_migration_stop";
14914
+ var NOTE = "sphica_migration_note";
14844
14915
  var KEEP = 3;
14845
14916
  function backUp(raw, file, from) {
14846
14917
  fs5.mkdirSync(backupDir(file), { recursive: true, mode: 448 });
@@ -14880,6 +14951,46 @@ function prune(file, kept) {
14880
14951
  } catch {}
14881
14952
  }
14882
14953
  var versionOf = (raw) => raw.prepare("pragma user_version").get().user_version;
14954
+ var SQLITE_BUSY = 5;
14955
+ function taken(raw, table) {
14956
+ if (!raw.prepare("select 1 from temp.sqlite_schema where type = 'table' and name = ?").get(table))
14957
+ return [];
14958
+ const rows = raw.prepare(`select * from temp.${table} order by rule, rowid`).all();
14959
+ raw.exec(`drop table temp.${table}`);
14960
+ return rows;
14961
+ }
14962
+ function listed(rows) {
14963
+ const rules = new Map;
14964
+ for (const r2 of rows) {
14965
+ const of = rules.get(r2.rule);
14966
+ if (of)
14967
+ of.push(r2);
14968
+ else
14969
+ rules.set(r2.rule, [r2]);
14970
+ }
14971
+ return [...rules].flatMap(([rule2, of]) => [
14972
+ `${inline(String(rule2))}: ${plural(of.length, "row")}`,
14973
+ ...of.map((r2) => ` ${inline(String(r2.item))}${r2.action ? ` → ${inline(String(r2.action))}` : ""}`)
14974
+ ]).join(`
14975
+ `);
14976
+ }
14977
+
14978
+ class Stop extends Error {
14979
+ list;
14980
+ constructor(revision, rows) {
14981
+ super(`The database has ${plural(rows.length, "row")} that revision ${revision} cannot take, and Sphica writes no such row (listed below). Fix the rows, or forget a listed source with /sphica:forget on the Sphica version that still opens this database, then run \`sphica init\` again`);
14982
+ this.list = listed(rows);
14983
+ }
14984
+ }
14985
+ function committed(raw) {
14986
+ if (raw.isTransaction)
14987
+ return null;
14988
+ try {
14989
+ return versionOf(raw);
14990
+ } catch {
14991
+ return null;
14992
+ }
14993
+ }
14883
14994
  function immediate(raw, fn) {
14884
14995
  raw.exec("begin immediate");
14885
14996
  try {
@@ -14891,6 +15002,11 @@ function immediate(raw, fn) {
14891
15002
  throw e;
14892
15003
  }
14893
15004
  }
15005
+ function optimize(raw) {
15006
+ try {
15007
+ raw.exec("pragma optimize");
15008
+ } catch {}
15009
+ }
14894
15010
  function withOwner(file, fn, create = false) {
14895
15011
  const raw = connectWriter("owner", file, create);
14896
15012
  try {
@@ -14910,27 +15026,53 @@ function migrate(file = dbFile(), dir = MIGRATIONS()) {
14910
15026
  for (let r2 = from + 1;r2 <= SCHEMA_REVISION; r2++) {
14911
15027
  const script = MIGRATION(dir, r2);
14912
15028
  if (!fs5.existsSync(script))
14913
- throw new Error(`No migration from revision ${r2 - 1}. Move the database aside, then run \`sphica init\`.`);
15029
+ throw new Error(`This Sphica has no migration script for revision ${r2} (${script} is missing). Reinstall sphica (\`npm i -g sphica@${packageVersionAt(ROOT) ?? "latest"}\`), then run \`sphica init\` again.`);
14914
15030
  raw.exec("pragma foreign_keys = off");
14915
15031
  try {
14916
- immediate(raw, () => {
15032
+ const notes = immediate(raw, () => {
14917
15033
  if (versionOf(raw) !== r2 - 1)
14918
- return;
15034
+ return [];
15035
+ if (fs5.existsSync(CHECK(dir, r2))) {
15036
+ raw.exec(fs5.readFileSync(CHECK(dir, r2), "utf8"));
15037
+ const stops = taken(raw, STOP2);
15038
+ if (stops.length)
15039
+ throw new Stop(r2, stops);
15040
+ }
14919
15041
  raw.exec(fs5.readFileSync(script, "utf8"));
14920
15042
  const broken = raw.prepare("pragma foreign_key_check").all().length;
14921
15043
  if (broken)
14922
15044
  throw new Error(`Migration to revision ${r2} left ${plural(broken, "broken reference")}; nothing was changed`);
14923
15045
  if (versionOf(raw) !== r2)
14924
15046
  throw new Error(`${path5.basename(script)} did not set revision ${r2}`);
15047
+ return taken(raw, NOTE);
14925
15048
  });
15049
+ if (notes.length)
15050
+ say(`Changed while migrating to revision ${r2}: ${plural(notes.length, "row")} (the backup keeps them as they were)
15051
+ ${listed(notes)}`);
14926
15052
  } finally {
14927
15053
  raw.exec("pragma foreign_keys = on");
14928
15054
  }
14929
15055
  }
14930
15056
  } catch (e) {
15057
+ const said = e.message.replace(/\.$/, "");
15058
+ const list = e instanceof Stop ? `
15059
+ ${e.list}` : "";
15060
+ const now = committed(raw);
15061
+ if (now === from) {
15062
+ let left = "the backup made for this run was removed";
15063
+ try {
15064
+ fs5.rmSync(backup, { force: true });
15065
+ } catch {
15066
+ left = `the backup made for this run is still at ${backup}; delete it yourself`;
15067
+ }
15068
+ const retry = sqliteCode(e) === SQLITE_BUSY ? " Another process is writing to the database. Run `sphica init` again when it has finished." : "";
15069
+ throw new Error(`${said}. No migration step was committed: the database is still at revision ${from}, and there is no need to replace it with a backup (${left}).${retry}${list}`);
15070
+ }
14931
15071
  const base = path5.basename(file);
14932
- throw new Error(`${e.message}. The database before migrating is at ${backup} (anything recorded after it was made is not in it). To go back to it, close every session using Sphica, move ${base}, ${base}-wal, and ${base}-shm in ${path5.dirname(file)} aside, then copy the backup to ${file}.`);
15072
+ const state = now === null ? "The committed revision could not be confirmed after the failure" : `The database is now at revision ${now} (this run, or another \`sphica init\` running at the same time, migrated it that far)`;
15073
+ throw new Error(`${said}. ${state}. The database before migrating is at ${backup} (anything recorded after it was made is not in it). To go back to it, close every session using Sphica, move ${base}, ${base}-wal, and ${base}-shm in ${path5.dirname(file)} aside, then copy the backup to ${file}.${list}`);
14933
15074
  }
15075
+ optimize(raw);
14934
15076
  prune(file, backup);
14935
15077
  return from;
14936
15078
  });
@@ -14966,6 +15108,7 @@ function dbInit(file = dbFile()) {
14966
15108
  raw.exec(fs5.readFileSync(SCHEMA(), "utf8"));
14967
15109
  if (versionOf(raw) !== SCHEMA_REVISION)
14968
15110
  throw new Error(`db/schema.sql has user_version ${versionOf(raw)}, but the code expects ${SCHEMA_REVISION}`);
15111
+ optimize(raw);
14969
15112
  }, true);
14970
15113
  try {
14971
15114
  fs5.linkSync(tmp, file);
@@ -14994,17 +15137,21 @@ function bindOwner(user, file = dbFile()) {
14994
15137
  }));
14995
15138
  }
14996
15139
  function reindex(file = dbFile()) {
14997
- const counts = withOwner(file, (raw) => immediate(raw, () => {
14998
- raw.exec("insert into unit_fts (unit_fts) values ('delete-all')");
14999
- raw.exec("insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text");
15000
- raw.exec("insert into source_fts (source_fts) values ('delete-all')");
15001
- raw.exec("insert into source_fts (rowid, lexemes) select id, sphica_terms(text) from source where indexed = 1");
15002
- const n2 = (sql2) => raw.prepare(sql2).get().n;
15003
- return {
15004
- units: n2("select count(*) as n from unit"),
15005
- sources: n2("select count(*) as n from source where indexed = 1")
15006
- };
15007
- }));
15140
+ const counts = withOwner(file, (raw) => {
15141
+ const n2 = immediate(raw, () => {
15142
+ raw.exec("insert into unit_fts (unit_fts) values ('delete-all')");
15143
+ raw.exec("insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text");
15144
+ raw.exec("insert into source_fts (source_fts) values ('delete-all')");
15145
+ raw.exec("insert into source_fts (rowid, lexemes) select id, sphica_terms(text) from source where indexed = 1");
15146
+ const n3 = (sql2) => raw.prepare(sql2).get().n;
15147
+ return {
15148
+ units: n3("select count(*) as n from unit"),
15149
+ sources: n3("select count(*) as n from source where indexed = 1")
15150
+ };
15151
+ });
15152
+ optimize(raw);
15153
+ return n2;
15154
+ });
15008
15155
  say(`Rebuilt the index: ${plural(counts.units, "unit")}, ${plural(counts.sources, "source")}`);
15009
15156
  }
15010
15157
  function inspect(file = dbFile()) {
@@ -15162,7 +15309,7 @@ function localRoots(roots = [path7.join(os3.homedir(), "Projects")]) {
15162
15309
  function relativeTo(root, file, cwd = root) {
15163
15310
  const abs = path7.resolve(cwd, file);
15164
15311
  const rel = path7.relative(root, abs);
15165
- if (!rel || leaves(rel))
15312
+ if (!rel || leaves(rel) || /\p{Cc}/u.test(rel))
15166
15313
  return null;
15167
15314
  return rel.split(path7.sep).join("/");
15168
15315
  }
@@ -15194,7 +15341,7 @@ var git2 = (root, args) => {
15194
15341
  return null;
15195
15342
  }
15196
15343
  };
15197
- var usable = (p) => p !== "" && !p.includes("\\");
15344
+ var usable = (p) => p !== "" && !p.includes("\\") && !/\p{Cc}/u.test(p);
15198
15345
  function snapshot(root) {
15199
15346
  const out = git2(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
15200
15347
  if (out === null)
@@ -15934,12 +16081,15 @@ async function doctor(cwd) {
15934
16081
  await withDb("reader", async (db) => {
15935
16082
  const owners = await db.selectFrom("owner_identity").select(["external_id", "login"]).where("provider", "=", "github").orderBy("bound_at").execute();
15936
16083
  say2(owners.length ? "ok" : "none", "GitHub owner", owners.length ? owners.map((o) => `${inline(o.login ?? "?")} (id ${inline(o.external_id)})`).join(" / ") : "none. Bind it with sphica init while gh is signed in");
16084
+ const twice = await db.selectFrom("unit").select("project_id").where("lifecycle", "in", ["active", "candidate"]).groupBy(["project_id", "content_hash"]).having((eb) => eb.fn.countAll(), ">", 1).execute();
16085
+ if (twice.length)
16086
+ say2("warn", "Records", `${plural(twice.length, "set")} of live records hold the same words. Read them with Sphica's read and withdraw the extra ones with /sphica:glean`);
15937
16087
  const { found } = localRoots();
15938
16088
  const rows = await db.selectFrom("project as p").select((eb) => [
15939
16089
  "p.key",
15940
16090
  "p.name",
15941
16091
  eb.selectFrom("unit as u").select((u3) => u3.fn.countAll().as("n")).whereRef("u.project_id", "=", "p.id").as("records"),
15942
- eb.selectFrom("extraction_run as r").select((r2) => r2.fn.max("r.finished_at").as("at")).whereRef("r.project_id", "=", "p.id").where("r.status", "=", "saved").as("extracted")
16092
+ eb.selectFrom("extraction_run as r").select((r2) => r2.fn.max("r.finished_at").as("at")).whereRef("r.project_id", "=", "p.id").where("r.status", "=", "saved").where("r.origin", "<>", "migration").as("extracted")
15943
16093
  ]).orderBy("p.name").execute();
15944
16094
  if (rows.length)
15945
16095
  console.log(section("Projects", true));
package/dist/deliver.js CHANGED
@@ -11134,7 +11134,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
11134
11134
 
11135
11135
  // server/src/sqlite.ts
11136
11136
  var SCHEMA_GENERATION = 2;
11137
- var SCHEMA_REVISION = 4;
11137
+ var SCHEMA_REVISION = 5;
11138
11138
  var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os.homedir(), ".sphica");
11139
11139
  var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
11140
11140
  function requireRuntime() {
@@ -11336,24 +11336,92 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
11336
11336
  return C2.SQLITE_DENY;
11337
11337
  }
11338
11338
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
11339
- var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
11340
- var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
11341
- function ingestAuthorizer(action, p1, p2) {
11339
+ var INGEST_INSERTS = new Set([
11340
+ "project",
11341
+ "ingest_source",
11342
+ "extraction_run",
11343
+ "source_processing",
11344
+ "unit",
11345
+ "unit_option",
11346
+ "unit_evidence",
11347
+ "unit_adoption",
11348
+ "unit_link",
11349
+ "unit_state",
11350
+ "unit_anchor",
11351
+ "unit_alias",
11352
+ "field_def",
11353
+ "unit_field",
11354
+ "work",
11355
+ "artifact_link"
11356
+ ]);
11357
+ var RETRACTION = [
11358
+ "retracted_at",
11359
+ "retraction_reason",
11360
+ "retraction_source_id",
11361
+ "retraction_span_start",
11362
+ "retraction_span_end"
11363
+ ];
11364
+ var INGEST_UPDATES = {
11365
+ extraction_run: new Set(["status", "finished_at"]),
11366
+ unit_anchor: new Set(["retired_at", "replaced_by"]),
11367
+ unit_link: new Set(["resolved_at", "resolution"]),
11368
+ unit_evidence: new Set(RETRACTION),
11369
+ unit_adoption: new Set(RETRACTION),
11370
+ work: new Set(["title", "goal", "current", "next", "status", "branch", "run_id", "updated_at"])
11371
+ };
11372
+ var INGEST_DELETES = new Set(["artifact_link"]);
11373
+ var INGEST_TRIGGER_WRITES = {
11374
+ ingest_source_insert: ["insert source"],
11375
+ source_fts_ai: ["insert source_fts"],
11376
+ source_fts_ad: ["delete source_fts"],
11377
+ unit_state_apply: ["update unit"],
11378
+ unit_state_restore: ["insert unit_state"],
11379
+ ...Object.fromEntries([
11380
+ "evidence_i",
11381
+ "evidence_u",
11382
+ "evidence_d",
11383
+ "adoption_i",
11384
+ "adoption_u",
11385
+ "adoption_d",
11386
+ "link_i",
11387
+ "link_u",
11388
+ "anchor_i",
11389
+ "anchor_u",
11390
+ "alias_i",
11391
+ "field_i",
11392
+ "field_d"
11393
+ ].map((t) => [`unit_rev_${t}`, ["update unit"]])),
11394
+ unit_fts_ai: ["insert unit_fts"],
11395
+ unit_fts_ad: ["delete unit_fts"],
11396
+ ...Object.fromEntries(["option_i", "anchor_i", "anchor_u", "anchor_d", "alias_i", "alias_d", "field_i", "field_d"].map((t) => [
11397
+ `unit_fts_${t}`,
11398
+ ["delete unit_fts", "insert unit_fts"]
11399
+ ]))
11400
+ };
11401
+ var opOf = (action) => action === C2.SQLITE_INSERT ? "insert" : action === C2.SQLITE_UPDATE ? "update" : action === C2.SQLITE_DELETE ? "delete" : undefined;
11402
+ function ingestAuthorizer(action, p1, p2, trigger) {
11342
11403
  if (DDL().has(action))
11343
11404
  return C2.SQLITE_DENY;
11344
- if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
11345
- return C2.SQLITE_DENY;
11346
- if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
11347
- return C2.SQLITE_DENY;
11405
+ const op = opOf(action);
11406
+ if (op) {
11407
+ const table = p1 ?? "";
11408
+ if (SHADOW.test(table))
11409
+ return C2.SQLITE_OK;
11410
+ if (trigger !== null)
11411
+ return INGEST_TRIGGER_WRITES[trigger]?.includes(`${op} ${table}`) ? C2.SQLITE_OK : C2.SQLITE_DENY;
11412
+ const allowed = op === "insert" ? INGEST_INSERTS.has(table) : op === "update" ? INGEST_UPDATES[table]?.has(p2 ?? "") : INGEST_DELETES.has(table);
11413
+ return allowed ? C2.SQLITE_OK : C2.SQLITE_DENY;
11414
+ }
11348
11415
  if (action === C2.SQLITE_PRAGMA)
11349
11416
  return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
11350
- return C2.SQLITE_OK;
11417
+ if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_RECURSIVE || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
11418
+ return C2.SQLITE_OK;
11419
+ return C2.SQLITE_DENY;
11351
11420
  }
11352
11421
  var FORGET_WRITES = {
11353
11422
  [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts", "unit_fts"]),
11354
11423
  [C2.SQLITE_DELETE]: new Set([
11355
11424
  "source",
11356
- "external_reference",
11357
11425
  "unit_evidence",
11358
11426
  "unit_adoption",
11359
11427
  "source_processing",
@@ -11393,7 +11461,7 @@ function connectWriter(role, file = dbFile(), create = false) {
11393
11461
  throw e;
11394
11462
  }
11395
11463
  if (role === "ingest")
11396
- raw.setAuthorizer(ingestAuthorizer);
11464
+ raw.setAuthorizer((action, p1, p2, _db, trigger) => ingestAuthorizer(action, p1, p2, trigger));
11397
11465
  else if (role === "forget")
11398
11466
  raw.setAuthorizer(forgetAuthorizer);
11399
11467
  else if (role === "capture") {
@@ -11497,7 +11565,7 @@ async function projectId(db, key) {
11497
11565
  function relativeTo(root, file, cwd = root) {
11498
11566
  const abs = path4.resolve(cwd, file);
11499
11567
  const rel = path4.relative(root, abs);
11500
- if (!rel || leaves(rel))
11568
+ if (!rel || leaves(rel) || /\p{Cc}/u.test(rel))
11501
11569
  return null;
11502
11570
  return rel.split(path4.sep).join("/");
11503
11571
  }
@@ -11529,7 +11597,7 @@ var git2 = (root, args) => {
11529
11597
  return null;
11530
11598
  }
11531
11599
  };
11532
- var usable = (p) => p !== "" && !p.includes("\\");
11600
+ var usable = (p) => p !== "" && !p.includes("\\") && !/\p{Cc}/u.test(p);
11533
11601
  function snapshot(root) {
11534
11602
  const out = git2(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
11535
11603
  if (out === null)