@vaur94/agz-memory 0.4.0-beta.1 → 0.4.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/ARCHITECTURE.md +154 -156
- package/CHANGELOG.md +77 -16
- package/README.md +157 -141
- package/README.tr.md +158 -144
- package/dist/admin.js +221 -53
- package/dist/core.js +233 -122
- package/dist/server.js +626 -94
- package/dist/types/capture/contract.d.ts +2 -2
- package/dist/types/core.d.ts +1 -0
- package/dist/types/db/backup.d.ts +1 -1
- package/dist/types/db/migrations/v010.d.ts +2 -0
- package/dist/types/db/schema.d.ts +3 -2
- package/dist/types/server.d.ts +1 -1
- package/dist/types/store/capture.d.ts +5 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/version.d.ts +1 -0
- package/docs/backup-restore-runbook.md +109 -33
- package/docs/backup-restore-runbook.tr.md +148 -0
- package/package.json +4 -2
package/dist/admin.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/admin/index.ts
|
|
5
|
-
import { createHash as
|
|
5
|
+
import { createHash as createHash6 } from "crypto";
|
|
6
6
|
import { Database as Database3 } from "bun:sqlite";
|
|
7
7
|
import {
|
|
8
8
|
existsSync as existsSync4,
|
|
@@ -41,7 +41,8 @@ function normalizeProjectName(value) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/types.ts
|
|
44
|
-
var SCHEMA_VERSION =
|
|
44
|
+
var SCHEMA_VERSION = 10;
|
|
45
|
+
var KINDS = ["decision", "fact", "procedure", "context", "research", "preference", "task"];
|
|
45
46
|
var PREDICATES = ["SUPPORTS", "DERIVED_FROM", "PART_OF", "ABOUT", "PRECEDES", "SUPERSEDES"];
|
|
46
47
|
|
|
47
48
|
// src/db/backup.ts
|
|
@@ -103,9 +104,12 @@ function hasTable(db, table) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
// src/db/backup.ts
|
|
106
|
-
var BACKUP_FORMAT = "
|
|
107
|
+
var BACKUP_FORMAT = "agz-memory-backup/1";
|
|
107
108
|
function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, productVersion) {
|
|
108
109
|
const sourceHealth = assertHealthyDatabase(db);
|
|
110
|
+
if (sourceHealth.schemaVersion !== undefined && sourceHealth.schemaVersion !== sourceSchema) {
|
|
111
|
+
throw new Error(`backup source schema v${sourceSchema} does not match database schema v${sourceHealth.schemaVersion}`);
|
|
112
|
+
}
|
|
109
113
|
const checkpoint = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
110
114
|
if (checkpoint.busy !== 0)
|
|
111
115
|
throw new Error("database WAL checkpoint is busy");
|
|
@@ -133,6 +137,9 @@ function createVerifiedBackup(db, databasePath, sourceSchema, targetSchema, prod
|
|
|
133
137
|
if (JSON.stringify(backupHealth.counts) !== JSON.stringify(sourceHealth.counts)) {
|
|
134
138
|
throw new Error("backup row counts differ from source database");
|
|
135
139
|
}
|
|
140
|
+
if (backupHealth.schemaVersion !== undefined && backupHealth.schemaVersion !== sourceSchema) {
|
|
141
|
+
throw new Error("backup schema does not match source schema");
|
|
142
|
+
}
|
|
136
143
|
const bytes = readFileSync(temporaryDatabasePath);
|
|
137
144
|
const manifest = {
|
|
138
145
|
format: BACKUP_FORMAT,
|
|
@@ -205,6 +212,9 @@ function verifyBackupManifest(manifestPath) {
|
|
|
205
212
|
if (JSON.stringify(health.counts) !== JSON.stringify(manifest.counts)) {
|
|
206
213
|
throw new Error("backup manifest row counts do not match");
|
|
207
214
|
}
|
|
215
|
+
if (health.schemaVersion !== undefined && health.schemaVersion !== manifest.sourceSchema) {
|
|
216
|
+
throw new Error("backup manifest source schema does not match database");
|
|
217
|
+
}
|
|
208
218
|
} finally {
|
|
209
219
|
db.close();
|
|
210
220
|
}
|
|
@@ -390,7 +400,7 @@ function acquireMigrationLock(databasePath, targetSchema, timeoutMs = 30000) {
|
|
|
390
400
|
rmSync2(path, { recursive: true, force: true });
|
|
391
401
|
throw error;
|
|
392
402
|
}
|
|
393
|
-
if (!
|
|
403
|
+
if (!isAlreadyExistsError(error))
|
|
394
404
|
throw error;
|
|
395
405
|
if (Date.now() >= deadline) {
|
|
396
406
|
const current = readMigrationLockOwner(path);
|
|
@@ -463,12 +473,84 @@ function processIsAlive(pid) {
|
|
|
463
473
|
return false;
|
|
464
474
|
}
|
|
465
475
|
}
|
|
476
|
+
function isAlreadyExistsError(error) {
|
|
477
|
+
return Boolean(error && typeof error === "object" && "code" in error && String(error.code) === "EEXIST");
|
|
478
|
+
}
|
|
466
479
|
|
|
467
480
|
// src/db/migrations/v009.ts
|
|
468
481
|
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
469
482
|
|
|
483
|
+
// src/capture/contract.ts
|
|
484
|
+
import * as z from "zod/v4";
|
|
485
|
+
var CAPTURE_SCHEMA = "agz-memory.capture/1";
|
|
486
|
+
var SUPPORTED_OPENCODE_VERSION = "0.0.0-beta-18743";
|
|
487
|
+
var CAPTURE_EVENT_MAX_BYTES = 16 * 1024;
|
|
488
|
+
var CAPTURE_CONTENT_MAX_CHARACTERS = 4800;
|
|
489
|
+
var CAPTURE_SUMMARY_MAX_CHARACTERS = 1200;
|
|
490
|
+
var CAPTURE_SUBJECT_MAX_CHARACTERS = 240;
|
|
491
|
+
var candidateSchema = z.object({
|
|
492
|
+
kind: z.enum(KINDS),
|
|
493
|
+
title: z.string().min(1).max(240),
|
|
494
|
+
summary: z.string().min(1).max(CAPTURE_SUMMARY_MAX_CHARACTERS),
|
|
495
|
+
content: z.string().min(1).max(CAPTURE_CONTENT_MAX_CHARACTERS),
|
|
496
|
+
subjectKey: z.string().min(1).max(CAPTURE_SUBJECT_MAX_CHARACTERS).optional(),
|
|
497
|
+
intent: z.enum(["create", "supersede", "ignore", "review"]),
|
|
498
|
+
targetNoteID: z.string().min(1).max(240).optional(),
|
|
499
|
+
confidence: z.number().finite().min(0).max(1),
|
|
500
|
+
evidence: z.enum(["explicit-user", "verified-outcome", "session-summary"])
|
|
501
|
+
}).strict();
|
|
502
|
+
var signalSchema = z.object({
|
|
503
|
+
tool: z.string().min(1).max(160),
|
|
504
|
+
status: z.enum(["completed", "error"]),
|
|
505
|
+
errorType: z.string().min(1).max(160).optional()
|
|
506
|
+
}).strict();
|
|
507
|
+
var captureEventSchema = z.object({
|
|
508
|
+
schema: z.literal(CAPTURE_SCHEMA),
|
|
509
|
+
idempotencyKey: z.string().regex(/^[0-9a-f]{64}$/),
|
|
510
|
+
projectID: z.uuid(),
|
|
511
|
+
bindingKey: z.string().regex(/^[0-9a-f]{64}$/),
|
|
512
|
+
kind: z.enum(["user-candidate", "assistant-candidate", "session-summary", "tool-signal"]),
|
|
513
|
+
source: z.object({
|
|
514
|
+
system: z.literal("opencode-v2"),
|
|
515
|
+
opencodeVersion: z.literal(SUPPORTED_OPENCODE_VERSION),
|
|
516
|
+
pluginVersion: z.string().min(1).max(80),
|
|
517
|
+
sessionID: z.string().min(1).max(240),
|
|
518
|
+
messageID: z.string().min(1).max(240).optional(),
|
|
519
|
+
ordinal: z.number().int().nonnegative().optional(),
|
|
520
|
+
toolCallID: z.string().min(1).max(240).optional(),
|
|
521
|
+
observedAt: z.number().int().nonnegative()
|
|
522
|
+
}).strict(),
|
|
523
|
+
candidate: candidateSchema.optional(),
|
|
524
|
+
signal: signalSchema.optional(),
|
|
525
|
+
redaction: z.object({
|
|
526
|
+
policyVersion: z.string().min(1).max(80),
|
|
527
|
+
replacements: z.number().int().nonnegative(),
|
|
528
|
+
truncated: z.boolean()
|
|
529
|
+
}).strict()
|
|
530
|
+
}).strict().superRefine((event, context) => {
|
|
531
|
+
if (event.kind === "tool-signal" && !event.signal) {
|
|
532
|
+
context.addIssue({ code: "custom", message: "tool-signal requires signal" });
|
|
533
|
+
}
|
|
534
|
+
if (event.kind !== "tool-signal" && !event.candidate) {
|
|
535
|
+
context.addIssue({ code: "custom", message: `${event.kind} requires candidate` });
|
|
536
|
+
}
|
|
537
|
+
if (event.kind === "tool-signal" && event.candidate) {
|
|
538
|
+
context.addIssue({ code: "custom", message: "tool-signal cannot carry candidate" });
|
|
539
|
+
}
|
|
540
|
+
if (event.kind !== "tool-signal" && event.signal) {
|
|
541
|
+
context.addIssue({ code: "custom", message: `${event.kind} cannot carry signal` });
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
function parseCaptureEvent(value) {
|
|
545
|
+
const event = captureEventSchema.parse(value);
|
|
546
|
+
if (Buffer.byteLength(JSON.stringify(event), "utf8") > CAPTURE_EVENT_MAX_BYTES) {
|
|
547
|
+
throw new Error(`capture event exceeds ${CAPTURE_EVENT_MAX_BYTES} UTF-8 bytes`);
|
|
548
|
+
}
|
|
549
|
+
return event;
|
|
550
|
+
}
|
|
551
|
+
|
|
470
552
|
// src/db/schema.ts
|
|
471
|
-
var
|
|
553
|
+
var SCHEMA_TABLES = `
|
|
472
554
|
CREATE TABLE IF NOT EXISTS projects (
|
|
473
555
|
id TEXT PRIMARY KEY,
|
|
474
556
|
name TEXT NOT NULL,
|
|
@@ -538,28 +620,7 @@ CREATE TABLE IF NOT EXISTS capture_checkpoints (
|
|
|
538
620
|
);
|
|
539
621
|
CREATE INDEX IF NOT EXISTS capture_checkpoints_due_idx
|
|
540
622
|
ON capture_checkpoints(state, next_reconcile_at);
|
|
541
|
-
|
|
542
|
-
idempotency_key TEXT PRIMARY KEY,
|
|
543
|
-
contract TEXT NOT NULL CHECK (contract = 'opencode2-memory.capture/1'),
|
|
544
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
545
|
-
binding_key TEXT NOT NULL REFERENCES project_bindings(binding_key) ON DELETE CASCADE,
|
|
546
|
-
event_kind TEXT NOT NULL CHECK (event_kind IN ('user-candidate','assistant-candidate','session-summary','tool-signal')),
|
|
547
|
-
source_session_id TEXT NOT NULL,
|
|
548
|
-
source_message_id TEXT,
|
|
549
|
-
source_ordinal INTEGER CHECK (source_ordinal IS NULL OR source_ordinal >= 0),
|
|
550
|
-
source_tool_call_id TEXT,
|
|
551
|
-
payload_json TEXT CHECK (payload_json IS NULL OR json_valid(payload_json)),
|
|
552
|
-
payload_hash TEXT,
|
|
553
|
-
redaction_version TEXT NOT NULL,
|
|
554
|
-
state TEXT NOT NULL CHECK (state IN ('pending','shadowed','review','materialized','duplicate','ignored','rejected','quarantined','failed','dead')),
|
|
555
|
-
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
|
556
|
-
note_id TEXT,
|
|
557
|
-
last_error_code TEXT,
|
|
558
|
-
generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0),
|
|
559
|
-
created_at INTEGER NOT NULL,
|
|
560
|
-
updated_at INTEGER NOT NULL,
|
|
561
|
-
processed_at INTEGER
|
|
562
|
-
);
|
|
623
|
+
${captureEventsTable()}
|
|
563
624
|
CREATE INDEX IF NOT EXISTS capture_events_state_idx ON capture_events(state, updated_at);
|
|
564
625
|
CREATE INDEX IF NOT EXISTS capture_events_session_idx ON capture_events(project_id, source_session_id);
|
|
565
626
|
CREATE INDEX IF NOT EXISTS capture_events_note_idx ON capture_events(project_id, note_id);
|
|
@@ -643,15 +704,39 @@ CREATE TRIGGER IF NOT EXISTS notes_fts_au AFTER UPDATE OF title, summary, conten
|
|
|
643
704
|
VALUES (new.rowid, new.title, new.summary, new.content);
|
|
644
705
|
END;
|
|
645
706
|
`;
|
|
646
|
-
function
|
|
647
|
-
db.exec(
|
|
707
|
+
function createSchema(db) {
|
|
708
|
+
db.exec(SCHEMA_TABLES);
|
|
648
709
|
db.exec(FTS_V9);
|
|
649
710
|
db.query("DELETE FROM schema_state").run();
|
|
650
|
-
db.query("INSERT INTO schema_state(version) VALUES (
|
|
711
|
+
db.query("INSERT INTO schema_state(version) VALUES (10)").run();
|
|
651
712
|
}
|
|
652
713
|
function rebuildFts(db) {
|
|
653
714
|
db.query("INSERT INTO notes_fts(notes_fts) VALUES ('rebuild')").run();
|
|
654
715
|
}
|
|
716
|
+
function captureEventsTable(table = "capture_events") {
|
|
717
|
+
return `CREATE TABLE IF NOT EXISTS ${table} (
|
|
718
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
719
|
+
contract TEXT NOT NULL CHECK (contract = '${CAPTURE_SCHEMA}'),
|
|
720
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
721
|
+
binding_key TEXT NOT NULL REFERENCES project_bindings(binding_key) ON DELETE CASCADE,
|
|
722
|
+
event_kind TEXT NOT NULL CHECK (event_kind IN ('user-candidate','assistant-candidate','session-summary','tool-signal')),
|
|
723
|
+
source_session_id TEXT NOT NULL,
|
|
724
|
+
source_message_id TEXT,
|
|
725
|
+
source_ordinal INTEGER CHECK (source_ordinal IS NULL OR source_ordinal >= 0),
|
|
726
|
+
source_tool_call_id TEXT,
|
|
727
|
+
payload_json TEXT CHECK (payload_json IS NULL OR json_valid(payload_json)),
|
|
728
|
+
payload_hash TEXT,
|
|
729
|
+
redaction_version TEXT NOT NULL,
|
|
730
|
+
state TEXT NOT NULL CHECK (state IN ('pending','shadowed','review','materialized','duplicate','ignored','rejected','quarantined','failed','dead')),
|
|
731
|
+
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
|
732
|
+
note_id TEXT,
|
|
733
|
+
last_error_code TEXT,
|
|
734
|
+
generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0),
|
|
735
|
+
created_at INTEGER NOT NULL,
|
|
736
|
+
updated_at INTEGER NOT NULL,
|
|
737
|
+
processed_at INTEGER
|
|
738
|
+
);`;
|
|
739
|
+
}
|
|
655
740
|
|
|
656
741
|
// src/db/migrations/v009.ts
|
|
657
742
|
function migrateV8ToV9(db) {
|
|
@@ -720,7 +805,7 @@ function migrateV8ToV9(db) {
|
|
|
720
805
|
ALTER TABLE notes_v9 RENAME TO notes;
|
|
721
806
|
ALTER TABLE note_edges_v9 RENAME TO note_edges;
|
|
722
807
|
`);
|
|
723
|
-
db.exec(
|
|
808
|
+
db.exec(SCHEMA_TABLES);
|
|
724
809
|
for (const note of notes) {
|
|
725
810
|
const provenanceID = randomUUID3();
|
|
726
811
|
db.query(`
|
|
@@ -744,6 +829,57 @@ function noteContentHash(kind, title, summary, content) {
|
|
|
744
829
|
return createHash3("sha256").update(`${kind}\x00${title}\x00${summary}\x00${content}`, "utf8").digest("hex");
|
|
745
830
|
}
|
|
746
831
|
|
|
832
|
+
// src/db/migrations/v010.ts
|
|
833
|
+
import { createHash as createHash4 } from "crypto";
|
|
834
|
+
function migrateV9ToV10(db) {
|
|
835
|
+
const payloads = db.query("SELECT idempotency_key, payload_json FROM capture_events WHERE payload_json IS NOT NULL").all();
|
|
836
|
+
const migratedPayloads = payloads.map((row) => {
|
|
837
|
+
const event = JSON.parse(row.payload_json);
|
|
838
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) {
|
|
839
|
+
throw new Error(`capture event ${row.idempotency_key} payload is not an object`);
|
|
840
|
+
}
|
|
841
|
+
event.schema = CAPTURE_SCHEMA;
|
|
842
|
+
const payload = JSON.stringify(parseCaptureEvent(event));
|
|
843
|
+
return {
|
|
844
|
+
idempotencyKey: row.idempotency_key,
|
|
845
|
+
payload,
|
|
846
|
+
payloadHash: createHash4("sha256").update(payload, "utf8").digest("hex")
|
|
847
|
+
};
|
|
848
|
+
});
|
|
849
|
+
db.exec("DROP TABLE IF EXISTS capture_events_v10");
|
|
850
|
+
db.exec(captureEventsTable("capture_events_v10"));
|
|
851
|
+
db.query(`
|
|
852
|
+
INSERT INTO capture_events_v10
|
|
853
|
+
(idempotency_key, contract, project_id, binding_key, event_kind,
|
|
854
|
+
source_session_id, source_message_id, source_ordinal, source_tool_call_id,
|
|
855
|
+
payload_json, payload_hash, redaction_version, state, attempt_count,
|
|
856
|
+
note_id, last_error_code, generation, created_at, updated_at, processed_at)
|
|
857
|
+
SELECT idempotency_key, ?, project_id, binding_key, event_kind,
|
|
858
|
+
source_session_id, source_message_id, source_ordinal, source_tool_call_id,
|
|
859
|
+
payload_json, payload_hash, redaction_version, state, attempt_count,
|
|
860
|
+
note_id, last_error_code, generation, created_at, updated_at, processed_at
|
|
861
|
+
FROM capture_events
|
|
862
|
+
`).run(CAPTURE_SCHEMA);
|
|
863
|
+
const updatePayload = db.query(`
|
|
864
|
+
UPDATE capture_events_v10
|
|
865
|
+
SET payload_json = ?, payload_hash = ?
|
|
866
|
+
WHERE idempotency_key = ?
|
|
867
|
+
`);
|
|
868
|
+
for (const row of migratedPayloads) {
|
|
869
|
+
updatePayload.run(row.payload, row.payloadHash, row.idempotencyKey);
|
|
870
|
+
}
|
|
871
|
+
db.exec(`
|
|
872
|
+
DROP TABLE capture_events;
|
|
873
|
+
ALTER TABLE capture_events_v10 RENAME TO capture_events;
|
|
874
|
+
`);
|
|
875
|
+
db.exec(SCHEMA_TABLES);
|
|
876
|
+
db.query("DELETE FROM schema_state").run();
|
|
877
|
+
db.query("INSERT INTO schema_state(version) VALUES (10)").run();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/version.ts
|
|
881
|
+
var PRODUCT_VERSION = "0.4.1";
|
|
882
|
+
|
|
747
883
|
// src/db.ts
|
|
748
884
|
var DDL = `
|
|
749
885
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -785,13 +921,11 @@ CREATE INDEX IF NOT EXISTS note_edges_target_idx ON note_edges(project_id, targe
|
|
|
785
921
|
CREATE TABLE IF NOT EXISTS schema_state (version INTEGER PRIMARY KEY);
|
|
786
922
|
`;
|
|
787
923
|
function openMemoryDatabase(path) {
|
|
788
|
-
|
|
789
|
-
|
|
924
|
+
let db = openDatabase(path);
|
|
925
|
+
let dbOpen = true;
|
|
790
926
|
let lock;
|
|
791
927
|
let backup;
|
|
792
928
|
try {
|
|
793
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
794
|
-
db.exec("PRAGMA journal_mode=WAL");
|
|
795
929
|
const existingVersion = getSchemaVersion(db);
|
|
796
930
|
if (existingVersion && existingVersion.version > SCHEMA_VERSION) {
|
|
797
931
|
throw new Error(`database schema v${existingVersion.version} is newer than supported v${SCHEMA_VERSION}`);
|
|
@@ -799,19 +933,36 @@ function openMemoryDatabase(path) {
|
|
|
799
933
|
const hasExistingData = hasLegacyV2(db) || hasTable2(db, "notes") || Boolean(existingVersion);
|
|
800
934
|
if (!hasExistingData) {
|
|
801
935
|
db.exec("PRAGMA foreign_keys=ON");
|
|
802
|
-
db.transaction(() =>
|
|
936
|
+
db.transaction(() => createSchema(db))();
|
|
803
937
|
assertHealthyDatabase(db);
|
|
804
938
|
return { db, close: () => db.close() };
|
|
805
939
|
}
|
|
806
940
|
if ((existingVersion?.version ?? 0) < SCHEMA_VERSION) {
|
|
941
|
+
db.close();
|
|
942
|
+
dbOpen = false;
|
|
807
943
|
lock = acquireMigrationLock(path, SCHEMA_VERSION);
|
|
808
|
-
|
|
944
|
+
db = openDatabase(path);
|
|
945
|
+
dbOpen = true;
|
|
946
|
+
const migrationVersion = getSchemaVersion(db);
|
|
947
|
+
if (migrationVersion && migrationVersion.version > SCHEMA_VERSION) {
|
|
948
|
+
throw new Error(`database schema v${migrationVersion.version} is newer than supported v${SCHEMA_VERSION}`);
|
|
949
|
+
}
|
|
950
|
+
if ((migrationVersion?.version ?? 0) === SCHEMA_VERSION) {
|
|
951
|
+
lock.release();
|
|
952
|
+
lock = undefined;
|
|
953
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
954
|
+
db.exec(SCHEMA_TABLES);
|
|
955
|
+
db.exec(FTS_V9);
|
|
956
|
+
assertHealthyDatabase(db);
|
|
957
|
+
return { db, close: () => db.close() };
|
|
958
|
+
}
|
|
959
|
+
backup = createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
|
|
809
960
|
db.exec("PRAGMA foreign_keys=OFF");
|
|
810
|
-
if (!
|
|
961
|
+
if (!migrationVersion && hasLegacyV2(db)) {
|
|
811
962
|
db.exec(DDL);
|
|
812
963
|
db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
|
|
813
964
|
migrateFromV2(db, path);
|
|
814
|
-
} else if (!
|
|
965
|
+
} else if (!migrationVersion) {
|
|
815
966
|
db.exec(DDL);
|
|
816
967
|
db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
|
|
817
968
|
db.transaction(() => {
|
|
@@ -819,14 +970,18 @@ function openMemoryDatabase(path) {
|
|
|
819
970
|
db.query("DELETE FROM schema_state").run();
|
|
820
971
|
db.query("INSERT INTO schema_state (version) VALUES (8)").run();
|
|
821
972
|
})();
|
|
822
|
-
} else if (
|
|
973
|
+
} else if (migrationVersion.version < 8) {
|
|
823
974
|
db.exec(DDL);
|
|
824
975
|
db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
|
|
825
976
|
migrateToV8(db);
|
|
826
977
|
}
|
|
827
|
-
|
|
828
|
-
if (version < 9)
|
|
978
|
+
let version = getSchemaVersion(db)?.version ?? 8;
|
|
979
|
+
if (version < 9) {
|
|
829
980
|
db.transaction(() => migrateV8ToV9(db))();
|
|
981
|
+
version = 9;
|
|
982
|
+
}
|
|
983
|
+
if (version < 10)
|
|
984
|
+
db.transaction(() => migrateV9ToV10(db))();
|
|
830
985
|
db.exec("PRAGMA foreign_keys=ON");
|
|
831
986
|
if (db.query("PRAGMA foreign_keys").get().foreign_keys !== 1) {
|
|
832
987
|
throw new Error("failed to enable database foreign keys");
|
|
@@ -838,13 +993,14 @@ function openMemoryDatabase(path) {
|
|
|
838
993
|
return { db, close: () => db.close() };
|
|
839
994
|
}
|
|
840
995
|
db.exec("PRAGMA foreign_keys=ON");
|
|
841
|
-
db.exec(
|
|
996
|
+
db.exec(SCHEMA_TABLES);
|
|
842
997
|
db.exec(FTS_V9);
|
|
843
998
|
assertHealthyDatabase(db);
|
|
844
999
|
db.exec("PRAGMA foreign_keys=ON");
|
|
845
1000
|
return { db, close: () => db.close() };
|
|
846
1001
|
} catch (error) {
|
|
847
|
-
|
|
1002
|
+
if (dbOpen)
|
|
1003
|
+
db.close();
|
|
848
1004
|
if (backup) {
|
|
849
1005
|
try {
|
|
850
1006
|
restoreVerifiedBackup(backup.manifestPath, path, "RESTORE_DATABASE_FROM_VERIFIED_BACKUP");
|
|
@@ -857,6 +1013,18 @@ function openMemoryDatabase(path) {
|
|
|
857
1013
|
lock?.release();
|
|
858
1014
|
}
|
|
859
1015
|
}
|
|
1016
|
+
function openDatabase(path) {
|
|
1017
|
+
const db = new Database2(path, { create: true });
|
|
1018
|
+
try {
|
|
1019
|
+
chmodSync2(path, 384);
|
|
1020
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
1021
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
1022
|
+
return db;
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
db.close();
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
860
1028
|
function getSchemaVersion(db) {
|
|
861
1029
|
if (!hasTable2(db, "schema_state"))
|
|
862
1030
|
return;
|
|
@@ -1159,7 +1327,7 @@ function count(db, sql, ...bindings) {
|
|
|
1159
1327
|
}
|
|
1160
1328
|
|
|
1161
1329
|
// src/retrieval/derived.ts
|
|
1162
|
-
import { createHash as
|
|
1330
|
+
import { createHash as createHash5 } from "crypto";
|
|
1163
1331
|
|
|
1164
1332
|
// src/capture/redact.ts
|
|
1165
1333
|
var RULES = [
|
|
@@ -1190,15 +1358,15 @@ function redactText(value, options = {}) {
|
|
|
1190
1358
|
let replacements = 0;
|
|
1191
1359
|
let highRisk = 0;
|
|
1192
1360
|
const classes = {};
|
|
1193
|
-
for (const
|
|
1194
|
-
if (!
|
|
1361
|
+
for (const literal2 of options.denylist ?? []) {
|
|
1362
|
+
if (!literal2)
|
|
1195
1363
|
continue;
|
|
1196
|
-
const count2 = text.split(
|
|
1364
|
+
const count2 = text.split(literal2).length - 1;
|
|
1197
1365
|
if (count2 === 0)
|
|
1198
1366
|
continue;
|
|
1199
1367
|
replacements += count2;
|
|
1200
1368
|
classes.denylist = (classes.denylist ?? 0) + count2;
|
|
1201
|
-
text = text.replaceAll(
|
|
1369
|
+
text = text.replaceAll(literal2, "[REDACTED:denylist]");
|
|
1202
1370
|
}
|
|
1203
1371
|
for (const rule of RULES) {
|
|
1204
1372
|
text = text.replace(rule.pattern, () => {
|
|
@@ -1248,7 +1416,7 @@ function deriveDocument(source) {
|
|
|
1248
1416
|
const content = redactText(source.content);
|
|
1249
1417
|
if (title.quarantined || summary.quarantined || content.quarantined)
|
|
1250
1418
|
return;
|
|
1251
|
-
const contentHash =
|
|
1419
|
+
const contentHash = createHash5("sha256").update(`${source.kind}\x00${title.text}\x00${summary.text}\x00${content.text}`, "utf8").digest("hex");
|
|
1252
1420
|
return {
|
|
1253
1421
|
projectID: source.projectID,
|
|
1254
1422
|
noteID: source.noteID,
|
|
@@ -1282,7 +1450,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
|
|
|
1282
1450
|
const db = new Database3(databasePath);
|
|
1283
1451
|
try {
|
|
1284
1452
|
const version = schemaVersion(db);
|
|
1285
|
-
return createVerifiedBackup(db, databasePath, version, version,
|
|
1453
|
+
return createVerifiedBackup(db, databasePath, version, version, PRODUCT_VERSION);
|
|
1286
1454
|
} finally {
|
|
1287
1455
|
db.close();
|
|
1288
1456
|
lock.release();
|
|
@@ -1390,7 +1558,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
|
|
|
1390
1558
|
if (command === "backup" && subcommand === "prune") {
|
|
1391
1559
|
const entries = backupEntries(databasePath);
|
|
1392
1560
|
const root = resolve2(`${databasePath}.backup`);
|
|
1393
|
-
const digest =
|
|
1561
|
+
const digest = createHash6("sha256").update(`${resolve2(databasePath)}\x00${root}
|
|
1394
1562
|
${entries.map((entry) => `${basename2(entry.manifest)}\x00${basename2(entry.database)}\x00${entry.sha256}\x00${entry.size}\x00${entry.manifestHash}`).join(`
|
|
1395
1563
|
`)}`).digest("hex");
|
|
1396
1564
|
if (option(argv, "--confirm") !== "DELETE_VERIFIED_BACKUPS") {
|
|
@@ -1472,7 +1640,7 @@ function verifiedBackupEntry(root, manifest) {
|
|
|
1472
1640
|
database: verified.databasePath,
|
|
1473
1641
|
sha256: verified.manifest.sha256,
|
|
1474
1642
|
size: verified.manifest.size,
|
|
1475
|
-
manifestHash:
|
|
1643
|
+
manifestHash: createHash6("sha256").update(bytes).digest("hex")
|
|
1476
1644
|
};
|
|
1477
1645
|
}
|
|
1478
1646
|
if (import.meta.main) {
|