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/cli.js
CHANGED
|
@@ -14187,7 +14187,7 @@ import os from "node:os";
|
|
|
14187
14187
|
import path2 from "node:path";
|
|
14188
14188
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
14189
14189
|
var SCHEMA_GENERATION = 2;
|
|
14190
|
-
var SCHEMA_REVISION =
|
|
14190
|
+
var SCHEMA_REVISION = 2;
|
|
14191
14191
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
14192
14192
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
14193
14193
|
function requireRuntime() {
|
|
@@ -14211,7 +14211,7 @@ function prepare(raw, check) {
|
|
|
14211
14211
|
const got = raw.prepare("pragma user_version").get()?.user_version;
|
|
14212
14212
|
if (got === SCHEMA_REVISION)
|
|
14213
14213
|
return;
|
|
14214
|
-
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "
|
|
14214
|
+
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."));
|
|
14215
14215
|
}
|
|
14216
14216
|
function generationOf(raw) {
|
|
14217
14217
|
const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
|
|
@@ -14383,22 +14383,54 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
14383
14383
|
return C2.SQLITE_DENY;
|
|
14384
14384
|
}
|
|
14385
14385
|
var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
|
|
14386
|
+
var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
|
|
14387
|
+
var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
|
|
14386
14388
|
function ingestAuthorizer(action, p1, p2) {
|
|
14387
14389
|
if (DDL().has(action))
|
|
14388
14390
|
return C2.SQLITE_DENY;
|
|
14389
|
-
if (writes(action) && p1 === "owner_identity")
|
|
14391
|
+
if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
|
|
14392
|
+
return C2.SQLITE_DENY;
|
|
14393
|
+
if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
|
|
14390
14394
|
return C2.SQLITE_DENY;
|
|
14391
14395
|
if (action === C2.SQLITE_PRAGMA)
|
|
14392
14396
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14393
14397
|
return C2.SQLITE_OK;
|
|
14394
14398
|
}
|
|
14399
|
+
var FORGET_WRITES = {
|
|
14400
|
+
[C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts"]),
|
|
14401
|
+
[C2.SQLITE_DELETE]: new Set([
|
|
14402
|
+
"source",
|
|
14403
|
+
"external_reference",
|
|
14404
|
+
"unit_evidence",
|
|
14405
|
+
"unit_adoption",
|
|
14406
|
+
"source_processing",
|
|
14407
|
+
"source_fts"
|
|
14408
|
+
])
|
|
14409
|
+
};
|
|
14410
|
+
var FORGET_UPDATES = {
|
|
14411
|
+
unit: new Set(["lifecycle", "revision"]),
|
|
14412
|
+
unit_state: new Set(["source_id"])
|
|
14413
|
+
};
|
|
14414
|
+
function forgetAuthorizer(action, p1, p2) {
|
|
14415
|
+
if (SHADOW.test(p1 ?? "") && writes(action))
|
|
14416
|
+
return C2.SQLITE_OK;
|
|
14417
|
+
if (action === C2.SQLITE_UPDATE)
|
|
14418
|
+
return FORGET_UPDATES[p1 ?? ""]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14419
|
+
if (action === C2.SQLITE_INSERT || action === C2.SQLITE_DELETE)
|
|
14420
|
+
return FORGET_WRITES[action]?.has(p1 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14421
|
+
if (action === C2.SQLITE_PRAGMA)
|
|
14422
|
+
return readsDataVersion(p1, p2) || p1 === "secure_delete" && ["on", "1", "true"].includes((p2 ?? "").toLowerCase()) || p1 === "wal_checkpoint" ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14423
|
+
if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
14424
|
+
return C2.SQLITE_OK;
|
|
14425
|
+
return C2.SQLITE_DENY;
|
|
14426
|
+
}
|
|
14395
14427
|
function connectWriter(role, file = dbFile(), create = false) {
|
|
14396
14428
|
requireRuntime();
|
|
14397
14429
|
if (!create)
|
|
14398
14430
|
requireFile(file);
|
|
14399
14431
|
const raw = new DatabaseSync2(file);
|
|
14400
14432
|
try {
|
|
14401
|
-
prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
|
|
14433
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
14402
14434
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
14403
14435
|
} catch (e) {
|
|
14404
14436
|
raw.close();
|
|
@@ -14406,6 +14438,8 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
14406
14438
|
}
|
|
14407
14439
|
if (role === "ingest")
|
|
14408
14440
|
raw.setAuthorizer(ingestAuthorizer);
|
|
14441
|
+
else if (role === "forget")
|
|
14442
|
+
raw.setAuthorizer(forgetAuthorizer);
|
|
14409
14443
|
else if (role === "capture") {
|
|
14410
14444
|
let own = false;
|
|
14411
14445
|
raw.setAuthorizer((action, p1, p2, _db, triggerOrView) => captureAuthorizer(own, action, p1, p2, triggerOrView));
|
|
@@ -14431,6 +14465,7 @@ function openWriter(role, file = dbFile()) {
|
|
|
14431
14465
|
// server/src/admin.ts
|
|
14432
14466
|
var say = (text) => console.log(indent(text));
|
|
14433
14467
|
var SCHEMA = () => path3.join(dbDir(), "schema.sql");
|
|
14468
|
+
var MIGRATION = (revision) => path3.join(dbDir(), "migrations", `${String(revision).padStart(4, "0")}.sql`);
|
|
14434
14469
|
var versionOf = (raw) => raw.prepare("pragma user_version").get().user_version;
|
|
14435
14470
|
function immediate(raw, fn) {
|
|
14436
14471
|
raw.exec("begin immediate");
|
|
@@ -14451,6 +14486,32 @@ function withOwner(file, fn, create = false) {
|
|
|
14451
14486
|
raw.close();
|
|
14452
14487
|
}
|
|
14453
14488
|
}
|
|
14489
|
+
function migrate(file = dbFile()) {
|
|
14490
|
+
return withOwner(file, (raw) => {
|
|
14491
|
+
const from = versionOf(raw);
|
|
14492
|
+
for (let r2 = from + 1;r2 <= SCHEMA_REVISION; r2++) {
|
|
14493
|
+
const script = MIGRATION(r2);
|
|
14494
|
+
if (!fs3.existsSync(script))
|
|
14495
|
+
throw new Error(`No migration from revision ${r2 - 1}. Move the database aside, then run \`sphica init\`.`);
|
|
14496
|
+
raw.exec("pragma foreign_keys = off");
|
|
14497
|
+
try {
|
|
14498
|
+
immediate(raw, () => {
|
|
14499
|
+
if (versionOf(raw) !== r2 - 1)
|
|
14500
|
+
return;
|
|
14501
|
+
raw.exec(fs3.readFileSync(script, "utf8"));
|
|
14502
|
+
const broken = raw.prepare("pragma foreign_key_check").all().length;
|
|
14503
|
+
if (broken)
|
|
14504
|
+
throw new Error(`Migration to revision ${r2} left ${plural(broken, "broken reference")}; nothing was changed`);
|
|
14505
|
+
if (versionOf(raw) !== r2)
|
|
14506
|
+
throw new Error(`${path3.basename(script)} did not set revision ${r2}`);
|
|
14507
|
+
});
|
|
14508
|
+
} finally {
|
|
14509
|
+
raw.exec("pragma foreign_keys = on");
|
|
14510
|
+
}
|
|
14511
|
+
}
|
|
14512
|
+
return from;
|
|
14513
|
+
});
|
|
14514
|
+
}
|
|
14454
14515
|
function dbInit(file = dbFile()) {
|
|
14455
14516
|
if (fs3.existsSync(file)) {
|
|
14456
14517
|
const look = new DatabaseSync3(file, { readOnly: true });
|
|
@@ -14465,8 +14526,12 @@ function dbInit(file = dbFile()) {
|
|
|
14465
14526
|
const got = withOwner(file, versionOf);
|
|
14466
14527
|
if (got === SCHEMA_REVISION)
|
|
14467
14528
|
say(`Already exists: ${file} (revision ${got})`);
|
|
14468
|
-
else
|
|
14469
|
-
say(`Already exists: ${file} (revision ${got}; this
|
|
14529
|
+
else if (got > SCHEMA_REVISION)
|
|
14530
|
+
say(`Already exists: ${file} (revision ${got}, made by a newer Sphica; this one expects ${SCHEMA_REVISION}. Update sphica.)`);
|
|
14531
|
+
else {
|
|
14532
|
+
migrate(file);
|
|
14533
|
+
say(`Migrated: ${file} (revision ${got} → ${SCHEMA_REVISION})`);
|
|
14534
|
+
}
|
|
14470
14535
|
return;
|
|
14471
14536
|
}
|
|
14472
14537
|
fs3.mkdirSync(path3.dirname(file), { recursive: true, mode: 448 });
|
|
@@ -15067,6 +15132,7 @@ function lock() {
|
|
|
15067
15132
|
return null;
|
|
15068
15133
|
}
|
|
15069
15134
|
var BATCH = 500;
|
|
15135
|
+
var FLUSH_BUDGET_MS = 30000;
|
|
15070
15136
|
var ROWS = 1000;
|
|
15071
15137
|
var chunks = (xs, n2 = ROWS) => Array.from({ length: Math.ceil(xs.length / n2) }, (_, i2) => xs.slice(i2 * n2, (i2 + 1) * n2));
|
|
15072
15138
|
async function write(db, batch, projects) {
|
|
@@ -15142,98 +15208,135 @@ async function write(db, batch, projects) {
|
|
|
15142
15208
|
}
|
|
15143
15209
|
var REJECTED = new Set([18, 19, 20, 25]);
|
|
15144
15210
|
var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
|
|
15145
|
-
|
|
15146
|
-
const unlock = lock();
|
|
15147
|
-
if (!unlock)
|
|
15148
|
-
return { sent: 0, deferred: 0, rejected: 0, busy: true };
|
|
15149
|
-
const dir = spoolDir();
|
|
15150
|
-
let client = null;
|
|
15211
|
+
var queued = (from) => {
|
|
15151
15212
|
try {
|
|
15152
|
-
|
|
15153
|
-
|
|
15213
|
+
return fs6.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
|
|
15214
|
+
} catch {
|
|
15215
|
+
return [];
|
|
15216
|
+
}
|
|
15217
|
+
};
|
|
15218
|
+
async function sendBatch(db, names) {
|
|
15219
|
+
const held = unregisteredDir();
|
|
15220
|
+
const records = [];
|
|
15221
|
+
for (const { name, from } of names) {
|
|
15222
|
+
let r2;
|
|
15223
|
+
try {
|
|
15224
|
+
r2 = current(JSON.parse(fs6.readFileSync(path7.join(from, name), "utf8")));
|
|
15225
|
+
} catch {
|
|
15226
|
+
fs6.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
15227
|
+
fs6.renameSync(path7.join(from, name), path7.join(rejectedDir(), name));
|
|
15228
|
+
continue;
|
|
15229
|
+
}
|
|
15230
|
+
if (r2)
|
|
15231
|
+
records.push({ name, from, r: r2 });
|
|
15232
|
+
else
|
|
15233
|
+
fs6.rmSync(path7.join(from, name), { force: true });
|
|
15234
|
+
}
|
|
15235
|
+
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 }]));
|
|
15236
|
+
const known = records.filter((x) => projects.has(x.r.project));
|
|
15237
|
+
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
15238
|
+
let sent = 0;
|
|
15239
|
+
const bad = [];
|
|
15240
|
+
try {
|
|
15241
|
+
sent = await write(db, known.map((x) => x.r), projects);
|
|
15242
|
+
} catch (e) {
|
|
15243
|
+
if (!rejected(e))
|
|
15244
|
+
throw e;
|
|
15245
|
+
for (const x of known) {
|
|
15154
15246
|
try {
|
|
15155
|
-
|
|
15156
|
-
} catch {
|
|
15157
|
-
|
|
15247
|
+
sent += await write(db, [x.r], projects);
|
|
15248
|
+
} catch (e2) {
|
|
15249
|
+
if (!rejected(e2))
|
|
15250
|
+
throw e2;
|
|
15251
|
+
bad.push(x);
|
|
15158
15252
|
}
|
|
15159
|
-
}
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
|
|
15163
|
-
const
|
|
15164
|
-
for (const { name, from } of names) {
|
|
15165
|
-
let r2;
|
|
15253
|
+
}
|
|
15254
|
+
}
|
|
15255
|
+
if (bad.length) {
|
|
15256
|
+
fs6.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
15257
|
+
for (const x of bad) {
|
|
15166
15258
|
try {
|
|
15167
|
-
|
|
15168
|
-
} catch {
|
|
15169
|
-
|
|
15170
|
-
|
|
15171
|
-
continue;
|
|
15172
|
-
}
|
|
15173
|
-
if (r2)
|
|
15174
|
-
records.push({ name, from, r: r2 });
|
|
15175
|
-
else
|
|
15176
|
-
fs6.rmSync(path7.join(from, name), { force: true });
|
|
15177
|
-
}
|
|
15178
|
-
const db = openWriter("capture", file);
|
|
15179
|
-
client = db;
|
|
15180
|
-
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 }]));
|
|
15181
|
-
const known = records.filter((x) => projects.has(x.r.project));
|
|
15182
|
-
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
15183
|
-
let sent = 0;
|
|
15184
|
-
const bad = [];
|
|
15185
|
-
try {
|
|
15186
|
-
sent = await write(db, known.map((x) => x.r), projects);
|
|
15187
|
-
} catch (e) {
|
|
15188
|
-
if (!rejected(e))
|
|
15189
|
-
throw e;
|
|
15190
|
-
for (const x of known) {
|
|
15191
|
-
try {
|
|
15192
|
-
sent += await write(db, [x.r], projects);
|
|
15193
|
-
} catch (e2) {
|
|
15194
|
-
if (!rejected(e2))
|
|
15195
|
-
throw e2;
|
|
15196
|
-
bad.push(x);
|
|
15197
|
-
}
|
|
15259
|
+
fs6.renameSync(path7.join(x.from, x.name), path7.join(rejectedDir(), x.name));
|
|
15260
|
+
} catch (e) {
|
|
15261
|
+
if (e.code !== "ENOENT")
|
|
15262
|
+
throw e;
|
|
15198
15263
|
}
|
|
15199
15264
|
}
|
|
15200
|
-
|
|
15201
|
-
|
|
15202
|
-
|
|
15203
|
-
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
|
|
15207
|
-
|
|
15208
|
-
|
|
15265
|
+
}
|
|
15266
|
+
if (strayed.length) {
|
|
15267
|
+
fs6.mkdirSync(held, { recursive: true, mode: 448 });
|
|
15268
|
+
for (const x of strayed) {
|
|
15269
|
+
if (x.from === held)
|
|
15270
|
+
continue;
|
|
15271
|
+
try {
|
|
15272
|
+
fs6.renameSync(path7.join(x.from, x.name), path7.join(held, x.name));
|
|
15273
|
+
} catch (e) {
|
|
15274
|
+
if (e.code !== "ENOENT")
|
|
15275
|
+
throw e;
|
|
15209
15276
|
}
|
|
15210
15277
|
}
|
|
15211
|
-
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15278
|
+
}
|
|
15279
|
+
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
15280
|
+
for (const x of records)
|
|
15281
|
+
if (!moved.has(x.name))
|
|
15282
|
+
fs6.rmSync(path7.join(x.from, x.name), { force: true });
|
|
15283
|
+
return { sent, rejected: bad.length };
|
|
15284
|
+
}
|
|
15285
|
+
async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
15286
|
+
const deadline = Date.now() + budgetMs;
|
|
15287
|
+
const dir = spoolDir();
|
|
15288
|
+
const held = unregisteredDir();
|
|
15289
|
+
const total = { sent: 0, deferred: 0, rejected: 0 };
|
|
15290
|
+
let batches = 0;
|
|
15291
|
+
let queueBatches = 0;
|
|
15292
|
+
for (let first = true;; first = false) {
|
|
15293
|
+
let unlock = lock();
|
|
15294
|
+
while (!unlock && Date.now() < deadline) {
|
|
15295
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
|
|
15296
|
+
if (Date.now() < deadline)
|
|
15297
|
+
unlock = lock();
|
|
15298
|
+
}
|
|
15299
|
+
if (!unlock)
|
|
15300
|
+
return first ? { ...total, busy: true } : total;
|
|
15301
|
+
let client = null;
|
|
15302
|
+
const send = async (names) => {
|
|
15303
|
+
client ??= openWriter("capture", file);
|
|
15304
|
+
const r2 = await sendBatch(client, names);
|
|
15305
|
+
total.sent += r2.sent;
|
|
15306
|
+
total.rejected += r2.rejected;
|
|
15307
|
+
batches++;
|
|
15308
|
+
};
|
|
15309
|
+
const late = () => batches > 0 && Date.now() >= deadline;
|
|
15310
|
+
const sentBefore = batches;
|
|
15311
|
+
try {
|
|
15312
|
+
if (first) {
|
|
15313
|
+
prune(held);
|
|
15314
|
+
for (const part of chunks(queued(held), BATCH)) {
|
|
15315
|
+
if (late())
|
|
15316
|
+
break;
|
|
15317
|
+
await send(part);
|
|
15221
15318
|
}
|
|
15222
15319
|
}
|
|
15320
|
+
while (queueBatches === 0 || !late()) {
|
|
15321
|
+
const part = queued(dir).slice(0, BATCH);
|
|
15322
|
+
if (part.length === 0)
|
|
15323
|
+
break;
|
|
15324
|
+
await send(part);
|
|
15325
|
+
queueBatches++;
|
|
15326
|
+
}
|
|
15327
|
+
prune(held);
|
|
15328
|
+
total.deferred = queued(held).length;
|
|
15329
|
+
if (batches > sentBefore)
|
|
15330
|
+
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
|
|
15331
|
+
} catch (e) {
|
|
15332
|
+
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
15333
|
+
throw e;
|
|
15334
|
+
} finally {
|
|
15335
|
+
await client?.destroy().catch(() => {});
|
|
15336
|
+
unlock();
|
|
15223
15337
|
}
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
if (!moved.has(x.name))
|
|
15227
|
-
fs6.rmSync(path7.join(x.from, x.name), { force: true });
|
|
15228
|
-
prune(held);
|
|
15229
|
-
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
15230
|
-
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
15231
|
-
} catch (e) {
|
|
15232
|
-
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
15233
|
-
throw e;
|
|
15234
|
-
} finally {
|
|
15235
|
-
await client?.destroy().catch(() => {});
|
|
15236
|
-
unlock();
|
|
15338
|
+
if (Date.now() >= deadline || queued(dir).length === 0)
|
|
15339
|
+
return total;
|
|
15237
15340
|
}
|
|
15238
15341
|
}
|
|
15239
15342
|
async function readInput(stream) {
|
|
@@ -15736,7 +15839,7 @@ async function doctor(cwd) {
|
|
|
15736
15839
|
const x = inspect(file);
|
|
15737
15840
|
usable2 = x.revision === SCHEMA_REVISION;
|
|
15738
15841
|
say2("ok", "DB", `${file} (${(x.bytes / 1024 / 1024).toFixed(1)} MB)`);
|
|
15739
|
-
say2(usable2 ? "ok" : "fail", "Schema version", usable2 ? `revision ${x.revision}` : `revision ${x.revision}, this Sphica expects ${SCHEMA_REVISION} (${x.revision < SCHEMA_REVISION ? "made by an older Sphica:
|
|
15842
|
+
say2(usable2 ? "ok" : "fail", "Schema version", usable2 ? `revision ${x.revision}` : `revision ${x.revision}, this Sphica expects ${SCHEMA_REVISION} (${x.revision < SCHEMA_REVISION ? "made by an older Sphica: run sphica init to migrate it" : "update sphica"})`);
|
|
15740
15843
|
const broken = Object.entries(x.fts).filter(([, v]) => v !== null);
|
|
15741
15844
|
say2(broken.length ? "fail" : "ok", "Full-text index", broken.length ? `broken: ${broken.map(([k, v]) => `${k} (${plain(v ?? "")})`).join(" / ")}. Rebuild it with sphica doctor --reindex` : "healthy");
|
|
15742
15845
|
} catch (e) {
|