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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/db/migrations/0005.check.sql +36 -0
- package/db/migrations/0005.sql +1172 -0
- package/db/schema.sql +230 -72
- package/dist/capture.js +81 -13
- package/dist/cli.js +179 -29
- package/dist/deliver.js +81 -13
- package/dist/mcp-record.js +184 -52
- package/dist/mcp.js +2 -4
- package/package.json +1 -1
- package/skills/glean/SKILL.md +1 -1
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 =
|
|
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
|
|
11291
|
-
|
|
11292
|
-
|
|
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
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
14758
|
-
|
|
14759
|
-
|
|
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
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
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) =>
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
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 =
|
|
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
|
|
11340
|
-
|
|
11341
|
-
|
|
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
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
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
|
-
|
|
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)
|