@fortemi/core 2026.9.6 → 2026.9.7

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/index.js CHANGED
@@ -6975,6 +6975,61 @@ var migration0033 = {
6975
6975
  `
6976
6976
  };
6977
6977
 
6978
+ // src/migrations/0034_lifecycle_purge_authority.ts
6979
+ init_geometry_buffer();
6980
+ var migration0034 = {
6981
+ version: 34,
6982
+ name: "0034_lifecycle_purge_authority",
6983
+ sql: `
6984
+ ALTER TABLE deletion_receipt RENAME TO deletion_receipt_legacy_0023;
6985
+
6986
+ CREATE TABLE lifecycle_purge_preview (
6987
+ id TEXT PRIMARY KEY,
6988
+ selector_fingerprint TEXT NOT NULL,
6989
+ selector JSONB NOT NULL,
6990
+ selected_note_ids JSONB NOT NULL,
6991
+ counts JSONB NOT NULL,
6992
+ expires_at TIMESTAMPTZ NOT NULL,
6993
+ consumed_by TEXT
6994
+ );
6995
+
6996
+ CREATE TABLE lifecycle_purge_operation (
6997
+ id TEXT PRIMARY KEY,
6998
+ preview_id TEXT NOT NULL UNIQUE REFERENCES lifecycle_purge_preview(id),
6999
+ selector_fingerprint TEXT NOT NULL,
7000
+ state TEXT NOT NULL CHECK (state IN ('cleanup_pending', 'completed')),
7001
+ counts JSONB NOT NULL,
7002
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
7003
+ completed_at TIMESTAMPTZ
7004
+ );
7005
+
7006
+ CREATE TABLE lifecycle_purge_erasure_target (
7007
+ operation_id TEXT NOT NULL REFERENCES lifecycle_purge_operation(id),
7008
+ note_id TEXT NOT NULL,
7009
+ PRIMARY KEY (operation_id, note_id)
7010
+ );
7011
+ CREATE INDEX idx_lifecycle_purge_erasure_note
7012
+ ON lifecycle_purge_erasure_target(note_id);
7013
+
7014
+ CREATE TABLE lifecycle_purge_blob_cleanup (
7015
+ operation_id TEXT NOT NULL REFERENCES lifecycle_purge_operation(id),
7016
+ blob_id TEXT NOT NULL,
7017
+ content_hash TEXT NOT NULL,
7018
+ completed_at TIMESTAMPTZ,
7019
+ PRIMARY KEY (operation_id, blob_id)
7020
+ );
7021
+
7022
+ CREATE TABLE deletion_receipt (
7023
+ operation_id TEXT PRIMARY KEY REFERENCES lifecycle_purge_operation(id),
7024
+ contract_version TEXT NOT NULL,
7025
+ outcome TEXT NOT NULL CHECK (outcome = 'completed'),
7026
+ counts JSONB NOT NULL,
7027
+ completed_at TIMESTAMPTZ NOT NULL,
7028
+ policy JSONB NOT NULL
7029
+ );
7030
+ `
7031
+ };
7032
+
6978
7033
  // src/migrations/index.ts
6979
7034
  var allMigrations = [
6980
7035
  migration0001,
@@ -7009,7 +7064,8 @@ var allMigrations = [
7009
7064
  migration0030,
7010
7065
  migration0031,
7011
7066
  migration0032,
7012
- migration0033
7067
+ migration0033,
7068
+ migration0034
7013
7069
  ];
7014
7070
 
7015
7071
  // src/data-archive.ts
@@ -7538,6 +7594,13 @@ var NotesRepository = class {
7538
7594
  */
7539
7595
  async restore(id) {
7540
7596
  await this.db.transaction(async (tx) => {
7597
+ const retained = await tx.query(
7598
+ "SELECT 1 FROM lifecycle_purge_erasure_target WHERE note_id = $1 LIMIT 1",
7599
+ [id]
7600
+ );
7601
+ if (retained.rows.length > 0) {
7602
+ throw new Error("Terminally purged notes cannot be restored");
7603
+ }
7541
7604
  await tx.query(
7542
7605
  `UPDATE note SET deleted_at = NULL, updated_at = now() WHERE id = $1`,
7543
7606
  [id]
@@ -24223,6 +24286,46 @@ function countOutcomes(outcomes) {
24223
24286
 
24224
24287
  // src/repositories/lifecycle-purge-repository.ts
24225
24288
  init_geometry_buffer();
24289
+
24290
+ // src/purge-selector.ts
24291
+ init_geometry_buffer();
24292
+ var LIFECYCLE_PURGE_MAX_NOTE_IDS = 500;
24293
+ function assertPurgeOpaqueId(value, label) {
24294
+ if (typeof value !== "string" || value.trim() !== value || value.length === 0 || value.length > 200) {
24295
+ throw new Error(`${label} must be a nonempty opaque identifier`);
24296
+ }
24297
+ }
24298
+ function validatePurgeSelector(selector) {
24299
+ const nonempty = (value) => typeof value === "string" && value.trim() === value && value.length > 0;
24300
+ if (!selector.note_ids && !selector.source || selector.note_ids !== void 0 && (!Array.isArray(selector.note_ids) || selector.note_ids.length === 0 || selector.note_ids.length > LIFECYCLE_PURGE_MAX_NOTE_IDS || !selector.note_ids.every(nonempty) || new Set(selector.note_ids).size !== selector.note_ids.length) || selector.source !== void 0 && (!nonempty(selector.source.namespace) || selector.source.namespace.length > 200 || selector.source.external_id !== void 0 && !nonempty(selector.source.external_id)) || selector.source?.external_id !== void 0 && selector.source.external_id.length > 1e3 || selector.tenant_id !== void 0 && !nonempty(selector.tenant_id) || selector.archive_id !== void 0 && selector.archive_id !== null && !nonempty(selector.archive_id)) {
24301
+ throw new Error("Purge selector must target nonempty note_ids or source identity");
24302
+ }
24303
+ }
24304
+ function purgeSelectorHash(selector) {
24305
+ validatePurgeSelector(selector);
24306
+ return computeHash(new TextEncoder().encode(JSON.stringify({
24307
+ selector_version: 2,
24308
+ source_scope_required: selector.tenant_id !== void 0 || selector.archive_id !== void 0 || selector.source !== void 0,
24309
+ tenant_id: selector.tenant_id ?? "default",
24310
+ archive_id: selector.archive_id ?? null,
24311
+ note_ids: [...selector.note_ids ?? []].sort(),
24312
+ source: selector.source ? {
24313
+ namespace: selector.source.namespace,
24314
+ external_id_hash: selector.source.external_id ? computeHash(new TextEncoder().encode(selector.source.external_id)) : null
24315
+ } : null
24316
+ })));
24317
+ }
24318
+
24319
+ // src/repositories/lifecycle-purge-repository.ts
24320
+ var LIFECYCLE_PURGE_CONTRACT_VERSION = "1.0.0";
24321
+ var LIFECYCLE_PURGE_PREVIEW_TTL_SECONDS = 900;
24322
+ var RECEIPT_POLICY = {
24323
+ authority: "Fortemi/fortemi#1092",
24324
+ mode: "terminal_purge",
24325
+ receipt_contains_content: false,
24326
+ backup_disposition: "beyond_use_then_reerase",
24327
+ restore_reerasure: true
24328
+ };
24226
24329
  function zeroCounts() {
24227
24330
  return {
24228
24331
  notes: 0,
@@ -24237,18 +24340,14 @@ function zeroCounts() {
24237
24340
  source_identities: 0
24238
24341
  };
24239
24342
  }
24240
- function selectorHash(selector) {
24241
- return computeHash(new TextEncoder().encode(JSON.stringify({
24242
- tenant_id: selector.tenant_id ?? "default",
24243
- archive_id: selector.archive_id ?? null,
24244
- note_ids: [...selector.note_ids ?? []].sort(),
24245
- source: selector.source ? {
24246
- namespace: selector.source.namespace,
24247
- external_id_hash: selector.source.external_id ? computeHash(new TextEncoder().encode(selector.source.external_id)) : null
24248
- } : null
24249
- })));
24343
+ function asCounts(value) {
24344
+ return { ...zeroCounts(), ...value };
24345
+ }
24346
+ function iso(value) {
24347
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
24250
24348
  }
24251
24349
  function buildSelectorWhere(selector, startIdx) {
24350
+ validatePurgeSelector(selector);
24252
24351
  const clauses = [];
24253
24352
  const params = [];
24254
24353
  let idx = startIdx;
@@ -24269,7 +24368,6 @@ function buildSelectorWhere(selector, startIdx) {
24269
24368
  if (selector.source) params.push(selector.source.namespace);
24270
24369
  if (selector.source?.external_id) params.push(selector.source.external_id);
24271
24370
  }
24272
- if (clauses.length === 0) throw new Error("Purge selector must target note_ids or source identity");
24273
24371
  return { sql: clauses.join(" AND "), params };
24274
24372
  }
24275
24373
  async function selectedNoteIds(db, selector) {
@@ -24280,129 +24378,375 @@ async function selectedNoteIds(db, selector) {
24280
24378
  );
24281
24379
  return result.rows.map((row) => row.id);
24282
24380
  }
24381
+ async function countSelected(db, noteIds) {
24382
+ const counts = zeroCounts();
24383
+ if (noteIds.length === 0) return counts;
24384
+ const queries = [
24385
+ "SELECT COUNT(*) AS count FROM note WHERE id = ANY($1)",
24386
+ `SELECT
24387
+ (SELECT COUNT(*) FROM note_revision WHERE note_id = ANY($1))
24388
+ + (SELECT COUNT(*) FROM note_original_history WHERE note_id = ANY($1)) AS count`,
24389
+ `SELECT
24390
+ (SELECT COUNT(*) FROM link WHERE source_note_id = ANY($1) OR target_note_id = ANY($1))
24391
+ + (SELECT COUNT(*) FROM link_url_target WHERE source_note_id = ANY($1)) AS count`,
24392
+ `SELECT
24393
+ (SELECT COUNT(*) FROM note_tag WHERE note_id = ANY($1))
24394
+ + (SELECT COUNT(*) FROM note_skos_tag WHERE note_id = ANY($1)) AS count`,
24395
+ `SELECT
24396
+ (SELECT COUNT(*) FROM embedding WHERE note_id = ANY($1))
24397
+ + (SELECT COUNT(*) FROM embedding_set_member WHERE note_id = ANY($1)) AS count`,
24398
+ "SELECT COUNT(*) AS count FROM attachment WHERE note_id = ANY($1)",
24399
+ `SELECT COUNT(*) AS count FROM attachment_blob ab
24400
+ WHERE EXISTS (SELECT 1 FROM attachment a
24401
+ WHERE (a.blob_id = ab.id OR a.preview_blob_id = ab.id) AND a.note_id = ANY($1))
24402
+ AND NOT EXISTS (SELECT 1 FROM attachment a
24403
+ WHERE (a.blob_id = ab.id OR a.preview_blob_id = ab.id) AND NOT (a.note_id = ANY($1)))`,
24404
+ "SELECT COUNT(*) AS count FROM graph_edge_artifact WHERE from_note_id = ANY($1) OR to_note_id = ANY($1)",
24405
+ `SELECT
24406
+ (SELECT COUNT(*) FROM provenance_edge
24407
+ WHERE note_id = ANY($1)
24408
+ OR revision_id IN (SELECT id FROM note_revision WHERE note_id = ANY($1)))
24409
+ + (SELECT COUNT(*) FROM provenance_derivation
24410
+ WHERE source_note_id = ANY($1)
24411
+ OR revision_id IN (SELECT id FROM note_revision WHERE note_id = ANY($1)))
24412
+ + (SELECT COUNT(*) FROM provenance_record
24413
+ WHERE note_id = ANY($1)
24414
+ OR attachment_id IN (SELECT id FROM attachment WHERE note_id = ANY($1))) AS count`,
24415
+ "SELECT COUNT(*) AS count FROM source_identity WHERE note_id = ANY($1)"
24416
+ ];
24417
+ const values = [];
24418
+ for (const sql of queries) {
24419
+ const result = await db.query(sql, [noteIds]);
24420
+ values.push(Number(result.rows[0]?.count ?? 0));
24421
+ }
24422
+ [
24423
+ counts.notes,
24424
+ counts.revisions,
24425
+ counts.links,
24426
+ counts.tags,
24427
+ counts.embeddings,
24428
+ counts.attachments,
24429
+ counts.blobs,
24430
+ counts.graph_edges,
24431
+ counts.provenance_edges,
24432
+ counts.source_identities
24433
+ ] = values;
24434
+ return counts;
24435
+ }
24436
+ async function deleteSelected(tx, noteIds, deleteSourceJournals = false) {
24437
+ if (noteIds.length === 0) return [];
24438
+ const params = [noteIds];
24439
+ const blobs = await tx.query(
24440
+ `SELECT ab.id, ab.content_hash FROM attachment_blob ab WHERE EXISTS (
24441
+ SELECT 1 FROM attachment a
24442
+ WHERE (a.blob_id = ab.id OR a.preview_blob_id = ab.id) AND a.note_id = ANY($1))
24443
+ AND NOT EXISTS (SELECT 1 FROM attachment a
24444
+ WHERE (a.blob_id = ab.id OR a.preview_blob_id = ab.id) AND NOT (a.note_id = ANY($1)))`,
24445
+ params
24446
+ );
24447
+ const sources = await tx.query(
24448
+ "SELECT DISTINCT tenant_id, archive_id, namespace FROM source_identity WHERE note_id = ANY($1)",
24449
+ params
24450
+ );
24451
+ await tx.query("DELETE FROM community_assignment WHERE note_id = ANY($1)", params);
24452
+ await tx.query("DELETE FROM graph_edge_artifact WHERE from_note_id = ANY($1) OR to_note_id = ANY($1)", params);
24453
+ await tx.query("DELETE FROM embedding_set_member WHERE note_id = ANY($1)", params);
24454
+ await tx.query("DELETE FROM embedding WHERE note_id = ANY($1)", params);
24455
+ await tx.query("DELETE FROM attachment_embedding WHERE attachment_id IN (SELECT id FROM attachment WHERE note_id = ANY($1))", params);
24456
+ await tx.query("DELETE FROM attachment WHERE note_id = ANY($1)", params);
24457
+ await tx.query(
24458
+ `DELETE FROM attachment_blob ab WHERE ab.id = ANY($1)
24459
+ AND NOT EXISTS (SELECT 1 FROM attachment a WHERE a.blob_id = ab.id OR a.preview_blob_id = ab.id)`,
24460
+ [blobs.rows.map((row) => row.id)]
24461
+ );
24462
+ await tx.query("DELETE FROM source_identity WHERE note_id = ANY($1)", params);
24463
+ if (deleteSourceJournals) {
24464
+ for (const source of sources.rows) {
24465
+ const scope = [source.tenant_id, source.archive_id, source.namespace];
24466
+ const stillOwned = await tx.query(
24467
+ `SELECT 1 FROM source_identity
24468
+ WHERE tenant_id = $1 AND archive_id IS NOT DISTINCT FROM $2 AND namespace = $3 LIMIT 1`,
24469
+ scope
24470
+ );
24471
+ if (stillOwned.rows.length === 0) {
24472
+ await tx.query(
24473
+ "DELETE FROM source_import_batch WHERE tenant_id = $1 AND archive_id IS NOT DISTINCT FROM $2 AND namespace = $3",
24474
+ scope
24475
+ );
24476
+ await tx.query(
24477
+ "DELETE FROM source_import_run WHERE tenant_id = $1 AND archive_id IS NOT DISTINCT FROM $2 AND namespace = $3",
24478
+ scope
24479
+ );
24480
+ }
24481
+ }
24482
+ }
24483
+ await tx.query(
24484
+ `DELETE FROM provenance_edge
24485
+ WHERE note_id = ANY($1)
24486
+ OR revision_id IN (SELECT id FROM note_revision WHERE note_id = ANY($1))`,
24487
+ params
24488
+ );
24489
+ await tx.query("DELETE FROM job_queue WHERE note_id = ANY($1)", params);
24490
+ await tx.query("DELETE FROM collection_note WHERE note_id = ANY($1)", params);
24491
+ await tx.query("DELETE FROM note_skos_tag WHERE note_id = ANY($1)", params);
24492
+ await tx.query("DELETE FROM note_tag WHERE note_id = ANY($1)", params);
24493
+ await tx.query(
24494
+ `DELETE FROM shard_field_presence
24495
+ WHERE (component = 'notes' AND record_id = ANY($1))
24496
+ OR (component = 'links' AND record_id IN (
24497
+ SELECT id FROM link WHERE source_note_id = ANY($1) OR target_note_id = ANY($1)
24498
+ UNION
24499
+ SELECT id FROM link_url_target WHERE source_note_id = ANY($1)
24500
+ ))`,
24501
+ params
24502
+ );
24503
+ await tx.query("DELETE FROM link_url_target WHERE source_note_id = ANY($1)", params);
24504
+ await tx.query("DELETE FROM link WHERE source_note_id = ANY($1) OR target_note_id = ANY($1)", params);
24505
+ await tx.query("DELETE FROM note_revision WHERE note_id = ANY($1)", params);
24506
+ await tx.query("DELETE FROM note_revised_current WHERE note_id = ANY($1)", params);
24507
+ await tx.query("DELETE FROM note_original WHERE note_id = ANY($1)", params);
24508
+ await tx.query("DELETE FROM note WHERE id = ANY($1)", params);
24509
+ return blobs.rows;
24510
+ }
24511
+ async function statusFrom(db, operationId) {
24512
+ const result = await db.query(
24513
+ `SELECT o.id, o.state, o.counts, r.counts AS receipt_counts,
24514
+ r.completed_at AS receipt_completed_at, r.policy AS receipt_policy
24515
+ ,(SELECT COUNT(*) FROM lifecycle_purge_blob_cleanup b
24516
+ WHERE b.operation_id = o.id AND b.completed_at IS NULL) AS blob_cleanup_pending
24517
+ FROM lifecycle_purge_operation o
24518
+ LEFT JOIN deletion_receipt r ON r.operation_id = o.id
24519
+ WHERE o.id = $1`,
24520
+ [operationId]
24521
+ );
24522
+ const row = result.rows[0];
24523
+ if (!row) return null;
24524
+ const receipt = row.receipt_completed_at === null ? void 0 : {
24525
+ contract_version: LIFECYCLE_PURGE_CONTRACT_VERSION,
24526
+ operation_id: row.id,
24527
+ outcome: "completed",
24528
+ counts: asCounts(row.receipt_counts),
24529
+ completed_at: iso(row.receipt_completed_at),
24530
+ policy: row.receipt_policy ?? RECEIPT_POLICY
24531
+ };
24532
+ return {
24533
+ contract_version: LIFECYCLE_PURGE_CONTRACT_VERSION,
24534
+ operation_id: row.id,
24535
+ outcome: row.state,
24536
+ counts: asCounts(row.counts),
24537
+ blob_cleanup_pending: Number(row.blob_cleanup_pending),
24538
+ search_cleanup_pending: false,
24539
+ ...receipt ? { receipt } : {}
24540
+ };
24541
+ }
24542
+ async function finalize(tx, operationId) {
24543
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
24544
+ await tx.query(
24545
+ `UPDATE lifecycle_purge_operation SET state = 'completed', completed_at = $2 WHERE id = $1`,
24546
+ [operationId, completedAt]
24547
+ );
24548
+ await tx.query(
24549
+ `INSERT INTO deletion_receipt (operation_id, contract_version, outcome, counts, completed_at, policy)
24550
+ SELECT id, $2, 'completed', counts, $3, $4::jsonb FROM lifecycle_purge_operation WHERE id = $1
24551
+ ON CONFLICT (operation_id) DO UPDATE SET outcome = 'completed', completed_at = $3`,
24552
+ [operationId, LIFECYCLE_PURGE_CONTRACT_VERSION, completedAt, JSON.stringify(RECEIPT_POLICY)]
24553
+ );
24554
+ }
24555
+ async function reeraseLifecyclePurgeTargets(tx, candidateNoteIds, trackBlobCleanup = false) {
24556
+ const capability = await tx.query(
24557
+ "SELECT to_regclass('lifecycle_purge_erasure_target')::text AS erasure_target"
24558
+ );
24559
+ if (capability.rows[0]?.erasure_target === null) {
24560
+ return { note_ids: [], operation_ids: [] };
24561
+ }
24562
+ const result = candidateNoteIds ? await tx.query(
24563
+ `SELECT DISTINCT t.note_id, t.operation_id
24564
+ FROM lifecycle_purge_erasure_target t JOIN note n ON n.id = t.note_id
24565
+ WHERE t.note_id = ANY($1) ORDER BY t.note_id`,
24566
+ [candidateNoteIds]
24567
+ ) : await tx.query(
24568
+ `SELECT DISTINCT t.note_id, t.operation_id
24569
+ FROM lifecycle_purge_erasure_target t JOIN note n ON n.id = t.note_id
24570
+ ORDER BY t.note_id`
24571
+ );
24572
+ if (result.rows.length === 0) return { note_ids: [], operation_ids: [] };
24573
+ const noteIds = [...new Set(result.rows.map((row) => row.note_id))];
24574
+ const operationIds = [...new Set(result.rows.map((row) => row.operation_id))];
24575
+ await tx.query("UPDATE lifecycle_purge_operation SET state = 'cleanup_pending', completed_at = NULL WHERE id = ANY($1)", [operationIds]);
24576
+ await tx.query("DELETE FROM deletion_receipt WHERE operation_id = ANY($1)", [operationIds]);
24577
+ const blobs = await deleteSelected(tx, noteIds);
24578
+ for (const operationId of operationIds) {
24579
+ for (const blob of trackBlobCleanup ? blobs : []) {
24580
+ await tx.query(
24581
+ `INSERT INTO lifecycle_purge_blob_cleanup
24582
+ (operation_id, blob_id, content_hash, completed_at)
24583
+ VALUES ($1, $2, $3, NULL)
24584
+ ON CONFLICT (operation_id, blob_id)
24585
+ DO UPDATE SET content_hash = $3, completed_at = NULL`,
24586
+ [operationId, blob.id, blob.content_hash]
24587
+ );
24588
+ }
24589
+ if (!trackBlobCleanup || blobs.length === 0) await finalize(tx, operationId);
24590
+ }
24591
+ return { note_ids: noteIds, operation_ids: operationIds };
24592
+ }
24283
24593
  var LifecyclePurgeRepository = class {
24284
- constructor(db, events) {
24594
+ constructor(db, events, blobStore) {
24285
24595
  this.db = db;
24286
24596
  this.events = events;
24597
+ this.blobStore = blobStore;
24287
24598
  }
24288
24599
  async preview(selector) {
24289
- const noteIds = await selectedNoteIds(this.db, selector);
24290
- return { selector_hash: selectorHash(selector), counts: await this.count(noteIds) };
24291
- }
24292
- async purge(selector, operationKey) {
24293
- const existing = await this.db.query(
24294
- `SELECT id, operation_key, tenant_id, archive_id, selector_hash, outcome, counts, completed_at, policy
24295
- FROM deletion_receipt
24296
- WHERE operation_key = $1`,
24297
- [operationKey]
24298
- );
24299
- if (existing.rows[0]) return existing.rows[0];
24300
- const hash = selectorHash(selector);
24301
- let receipt;
24302
- await this.db.transaction(async (tx) => {
24600
+ validatePurgeSelector(selector);
24601
+ return this.db.transaction(async (tx) => {
24602
+ await tx.query("DELETE FROM lifecycle_purge_preview WHERE consumed_by IS NULL AND expires_at <= now()");
24303
24603
  const noteIds = await selectedNoteIds(tx, selector);
24304
- const counts = await this.count(noteIds, tx);
24305
- await this.deleteSelected(tx, noteIds);
24306
- receipt = {
24307
- id: generateId(),
24308
- operation_key: operationKey,
24309
- tenant_id: selector.tenant_id ?? "default",
24310
- archive_id: selector.archive_id ?? null,
24311
- selector_hash: hash,
24312
- outcome: "completed",
24313
- counts,
24314
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
24315
- policy: {
24316
- authority: "fortemi#1092",
24317
- mode: "terminal-purge",
24318
- receipt_contains_content: false
24604
+ const counts = await countSelected(tx, noteIds);
24605
+ const previewId = generateId();
24606
+ const expiresAt = new Date(Date.now() + LIFECYCLE_PURGE_PREVIEW_TTL_SECONDS * 1e3).toISOString();
24607
+ await tx.query(
24608
+ `INSERT INTO lifecycle_purge_preview
24609
+ (id, selector_fingerprint, selector, selected_note_ids, counts, expires_at, consumed_by)
24610
+ VALUES ($1, $2, $3::jsonb, $4::jsonb, $5::jsonb, $6, NULL)`,
24611
+ [previewId, purgeSelectorHash(selector), JSON.stringify(selector), JSON.stringify(noteIds), JSON.stringify(counts), expiresAt]
24612
+ );
24613
+ return { contract_version: LIFECYCLE_PURGE_CONTRACT_VERSION, preview_id: previewId, counts, expires_at: expiresAt };
24614
+ });
24615
+ }
24616
+ async begin(request) {
24617
+ assertPurgeOpaqueId(request.operation_id, "operation_id");
24618
+ assertPurgeOpaqueId(request.preview_id, "preview_id");
24619
+ let created = false;
24620
+ const status = await this.db.transaction(async (tx) => {
24621
+ const existing = await tx.query("SELECT preview_id FROM lifecycle_purge_operation WHERE id = $1", [request.operation_id]);
24622
+ if (existing.rows[0]) {
24623
+ if (existing.rows[0].preview_id !== request.preview_id) throw new Error("Purge operation ID was already used for another preview");
24624
+ return await statusFrom(tx, request.operation_id);
24625
+ }
24626
+ const previewResult = await tx.query(
24627
+ `SELECT selector_fingerprint, selector, selected_note_ids, counts, expires_at, consumed_by
24628
+ FROM lifecycle_purge_preview WHERE id = $1`,
24629
+ [request.preview_id]
24630
+ );
24631
+ const preview = previewResult.rows[0];
24632
+ if (!preview) throw new Error("Lifecycle purge preview was not found");
24633
+ if (preview.consumed_by !== null) throw new Error("Lifecycle purge preview was already consumed");
24634
+ if (new Date(preview.expires_at).getTime() <= Date.now()) throw new Error("Lifecycle purge preview expired; create a new preview");
24635
+ validatePurgeSelector(preview.selector);
24636
+ if (purgeSelectorHash(preview.selector) !== preview.selector_fingerprint) throw new Error("Lifecycle purge preview integrity check failed");
24637
+ const currentIds = await selectedNoteIds(tx, preview.selector);
24638
+ const currentCounts = await countSelected(tx, currentIds);
24639
+ if (JSON.stringify(currentIds) !== JSON.stringify(preview.selected_note_ids) || JSON.stringify(currentCounts) !== JSON.stringify(asCounts(preview.counts))) {
24640
+ throw new Error("Lifecycle purge preview is stale; create a new preview");
24641
+ }
24642
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
24643
+ await tx.query(
24644
+ `INSERT INTO lifecycle_purge_operation
24645
+ (id, preview_id, selector_fingerprint, state, counts, created_at)
24646
+ VALUES ($1, $2, $3, 'cleanup_pending', $4::jsonb, $5)`,
24647
+ [request.operation_id, request.preview_id, preview.selector_fingerprint, JSON.stringify(currentCounts), now2]
24648
+ );
24649
+ for (const noteId of currentIds) {
24650
+ await tx.query(
24651
+ "INSERT INTO lifecycle_purge_erasure_target (operation_id, note_id) VALUES ($1, $2)",
24652
+ [request.operation_id, noteId]
24653
+ );
24654
+ }
24655
+ const blobs = await deleteSelected(
24656
+ tx,
24657
+ currentIds,
24658
+ preview.selector.source !== void 0 && preview.selector.source.external_id === void 0
24659
+ );
24660
+ if (this.blobStore) {
24661
+ for (const blob of blobs) {
24662
+ await tx.query(
24663
+ `INSERT INTO lifecycle_purge_blob_cleanup
24664
+ (operation_id, blob_id, content_hash, completed_at)
24665
+ VALUES ($1, $2, $3, NULL)`,
24666
+ [request.operation_id, blob.id, blob.content_hash]
24667
+ );
24319
24668
  }
24320
- };
24669
+ }
24321
24670
  await tx.query(
24322
- `INSERT INTO deletion_receipt
24323
- (id, operation_key, tenant_id, archive_id, selector_hash, outcome, counts, completed_at, policy)
24324
- VALUES ($1, $2, $3, $4, $5, 'completed', $6::jsonb, $7, $8::jsonb)`,
24325
- [
24326
- receipt.id,
24327
- receipt.operation_key,
24328
- receipt.tenant_id,
24329
- receipt.archive_id,
24330
- receipt.selector_hash,
24331
- JSON.stringify(receipt.counts),
24332
- receipt.completed_at,
24333
- JSON.stringify(receipt.policy)
24334
- ]
24671
+ `UPDATE lifecycle_purge_preview
24672
+ SET consumed_by = $2, selector = '{}'::jsonb, selected_note_ids = '[]'::jsonb
24673
+ WHERE id = $1`,
24674
+ [request.preview_id, request.operation_id]
24335
24675
  );
24676
+ if (!this.blobStore || blobs.length === 0) await finalize(tx, request.operation_id);
24677
+ created = true;
24678
+ return await statusFrom(tx, request.operation_id);
24336
24679
  });
24337
- const completedReceipt = receipt;
24338
- if (!completedReceipt) throw new Error("Purge transaction did not produce a receipt");
24339
- this.events?.emit("purge.completed", { receiptId: completedReceipt.id, counts: completedReceipt.counts });
24340
- return completedReceipt;
24341
- }
24342
- async count(noteIds, db = this.db) {
24343
- const counts = zeroCounts();
24344
- if (noteIds.length === 0) return counts;
24345
- const params = [noteIds];
24346
- const rows = await Promise.all([
24347
- db.query("SELECT COUNT(*) AS count FROM note WHERE id = ANY($1)", params),
24348
- db.query("SELECT COUNT(*) AS count FROM note_revision WHERE note_id = ANY($1)", params),
24349
- db.query("SELECT COUNT(*) AS count FROM link WHERE source_note_id = ANY($1) OR target_note_id = ANY($1)", params),
24350
- db.query("SELECT COUNT(*) AS count FROM note_tag WHERE note_id = ANY($1)", params),
24351
- db.query("SELECT COUNT(*) AS count FROM embedding WHERE note_id = ANY($1)", params),
24352
- db.query("SELECT COUNT(*) AS count FROM attachment WHERE note_id = ANY($1)", params),
24353
- db.query(
24354
- `SELECT COUNT(*) AS count FROM attachment_blob ab
24355
- WHERE EXISTS (SELECT 1 FROM attachment a WHERE a.blob_id = ab.id AND a.note_id = ANY($1))`,
24356
- params
24357
- ),
24358
- db.query("SELECT COUNT(*) AS count FROM graph_edge_artifact WHERE from_note_id = ANY($1) OR to_note_id = ANY($1)", params),
24359
- db.query(
24360
- `SELECT COUNT(*) AS count FROM provenance_edge
24361
- WHERE (entity_type = 'note' AND entity_id = ANY($1))
24362
- OR (attributes ->> 'note_id') = ANY($1)`,
24363
- params
24364
- ),
24365
- db.query("SELECT COUNT(*) AS count FROM source_identity WHERE note_id = ANY($1)", params)
24366
- ]);
24367
- const values = rows.map((row) => Number.parseInt(row.rows[0]?.count ?? "0", 10));
24368
- [
24369
- counts.notes,
24370
- counts.revisions,
24371
- counts.links,
24372
- counts.tags,
24373
- counts.embeddings,
24374
- counts.attachments,
24375
- counts.blobs,
24376
- counts.graph_edges,
24377
- counts.provenance_edges,
24378
- counts.source_identities
24379
- ] = values;
24380
- return counts;
24680
+ if (created && status.outcome === "completed") {
24681
+ this.events?.emit("purge.completed", {
24682
+ receiptId: status.operation_id,
24683
+ counts: status.counts
24684
+ });
24685
+ }
24686
+ return status;
24687
+ }
24688
+ async status(operationId) {
24689
+ assertPurgeOpaqueId(operationId, "operation_id");
24690
+ return statusFrom(this.db, operationId);
24691
+ }
24692
+ async resume(operationId) {
24693
+ assertPurgeOpaqueId(operationId, "operation_id");
24694
+ const initial = await this.status(operationId);
24695
+ if (!initial) throw new Error("Lifecycle purge operation was not found");
24696
+ if (initial.outcome === "completed") return initial;
24697
+ if (!this.blobStore) throw new Error("Lifecycle purge byte cleanup requires the configured BlobStore");
24698
+ const pending = await this.db.query(
24699
+ `SELECT blob_id, content_hash FROM lifecycle_purge_blob_cleanup
24700
+ WHERE operation_id = $1 AND completed_at IS NULL ORDER BY blob_id`,
24701
+ [operationId]
24702
+ );
24703
+ for (const blob of pending.rows) {
24704
+ if (!this.blobStore.delete) throw new Error("Lifecycle purge requires BlobStore.delete()");
24705
+ await this.blobStore.delete(blob.content_hash);
24706
+ await this.db.query(
24707
+ `UPDATE lifecycle_purge_blob_cleanup SET completed_at = $3
24708
+ WHERE operation_id = $1 AND blob_id = $2`,
24709
+ [operationId, blob.blob_id, (/* @__PURE__ */ new Date()).toISOString()]
24710
+ );
24711
+ }
24712
+ await this.db.transaction(async (tx) => {
24713
+ const remaining = await tx.query(
24714
+ "SELECT 1 FROM lifecycle_purge_blob_cleanup WHERE operation_id = $1 AND completed_at IS NULL LIMIT 1",
24715
+ [operationId]
24716
+ );
24717
+ if (remaining.rows.length === 0) await finalize(tx, operationId);
24718
+ });
24719
+ const completed = await this.status(operationId);
24720
+ if (completed.outcome === "completed") {
24721
+ this.events?.emit("purge.completed", {
24722
+ receiptId: completed.operation_id,
24723
+ counts: completed.counts
24724
+ });
24725
+ }
24726
+ return completed;
24381
24727
  }
24382
- async deleteSelected(tx, noteIds) {
24383
- if (noteIds.length === 0) return;
24384
- const params = [noteIds];
24385
- await tx.query("DELETE FROM community_assignment WHERE note_id = ANY($1)", params);
24386
- await tx.query("DELETE FROM graph_edge_artifact WHERE from_note_id = ANY($1) OR to_note_id = ANY($1)", params);
24387
- await tx.query("DELETE FROM embedding_set_member WHERE note_id = ANY($1)", params);
24388
- await tx.query("DELETE FROM embedding WHERE note_id = ANY($1)", params);
24389
- await tx.query("DELETE FROM attachment_embedding WHERE attachment_id IN (SELECT id FROM attachment WHERE note_id = ANY($1))", params);
24390
- await tx.query("DELETE FROM attachment WHERE note_id = ANY($1)", params);
24391
- await tx.query(
24392
- `DELETE FROM attachment_blob ab
24393
- WHERE NOT EXISTS (SELECT 1 FROM attachment a WHERE a.blob_id = ab.id)`
24394
- );
24395
- await tx.query("DELETE FROM source_identity WHERE note_id = ANY($1)", params);
24396
- await tx.query("DELETE FROM provenance_edge WHERE (entity_type = $2 AND entity_id = ANY($1)) OR (attributes ->> $3) = ANY($1)", [noteIds, "note", "note_id"]);
24397
- await tx.query("DELETE FROM job_queue WHERE note_id = ANY($1)", params);
24398
- await tx.query("DELETE FROM collection_note WHERE note_id = ANY($1)", params);
24399
- await tx.query("DELETE FROM note_tag WHERE note_id = ANY($1)", params);
24400
- await tx.query("DELETE FROM link WHERE source_note_id = ANY($1) OR target_note_id = ANY($1)", params);
24401
- await tx.query("DELETE FROM note_revision WHERE note_id = ANY($1)", params);
24402
- await tx.query("DELETE FROM note_revised_current WHERE note_id = ANY($1)", params);
24403
- await tx.query("DELETE FROM note_original WHERE note_id = ANY($1)", params);
24404
- await tx.query("DELETE FROM shard_field_presence WHERE component = $2 AND record_id = ANY($1)", [noteIds, "notes"]);
24405
- await tx.query("DELETE FROM note WHERE id = ANY($1)", params);
24728
+ /** Compatibility convenience: create and consume a fresh preview. */
24729
+ async purge(selector, operationId) {
24730
+ validatePurgeSelector(selector);
24731
+ assertPurgeOpaqueId(operationId, "operation_id");
24732
+ const existing = await this.db.query(
24733
+ "SELECT selector_fingerprint FROM lifecycle_purge_operation WHERE id = $1",
24734
+ [operationId]
24735
+ );
24736
+ if (existing.rows[0]) {
24737
+ if (existing.rows[0].selector_fingerprint !== purgeSelectorHash(selector)) {
24738
+ throw new Error("Purge operation key conflicts with selector");
24739
+ }
24740
+ let replay = await this.status(operationId);
24741
+ if (replay.outcome === "cleanup_pending") replay = await this.resume(operationId);
24742
+ if (!replay.receipt) throw new Error("Lifecycle purge did not produce a terminal receipt");
24743
+ return replay.receipt;
24744
+ }
24745
+ const preview = await this.preview(selector);
24746
+ let status = await this.begin({ operation_id: operationId, preview_id: preview.preview_id });
24747
+ if (status.outcome === "cleanup_pending") status = await this.resume(operationId);
24748
+ if (!status.receipt) throw new Error("Lifecycle purge did not produce a terminal receipt");
24749
+ return status.receipt;
24406
24750
  }
24407
24751
  };
24408
24752
 
@@ -24898,7 +25242,7 @@ function parseObject(value) {
24898
25242
  if (typeof value === "string") return JSON.parse(value);
24899
25243
  return value;
24900
25244
  }
24901
- function iso(value) {
25245
+ function iso2(value) {
24902
25246
  if (!value) return void 0;
24903
25247
  return value instanceof Date ? value.toISOString() : value;
24904
25248
  }
@@ -24990,7 +25334,7 @@ var CommunitiesRepository = class {
24990
25334
  graphSourceId: row.graph_source_id,
24991
25335
  searchQuery: parameters?.filters?.query,
24992
25336
  filters: parameters?.filters ?? void 0,
24993
- createdAt: iso(row.created_at),
25337
+ createdAt: iso2(row.created_at),
24994
25338
  freshness: freshness?.status ?? "unknown"
24995
25339
  };
24996
25340
  });
@@ -39140,7 +39484,7 @@ function jsonObject3(value) {
39140
39484
  if (typeof value === "string") return JSON.parse(value);
39141
39485
  return value;
39142
39486
  }
39143
- function iso2(value) {
39487
+ function iso3(value) {
39144
39488
  return value instanceof Date ? value.toISOString() : value;
39145
39489
  }
39146
39490
  function coreV1OptionErrors(options) {
@@ -39473,8 +39817,8 @@ async function exportShardBytes(db, options, mode) {
39473
39817
  ...row.extraction_status !== null ? { extraction_status: row.extraction_status } : {},
39474
39818
  ...row.extraction_reason !== null ? { reason: row.extraction_reason } : {},
39475
39819
  ...!coreV1 ? {
39476
- created_at: iso2(row.created_at),
39477
- deleted_at: row.deleted_at ? iso2(row.deleted_at) : null
39820
+ created_at: iso3(row.created_at),
39821
+ deleted_at: row.deleted_at ? iso3(row.deleted_at) : null
39478
39822
  } : {},
39479
39823
  attachment: {
39480
39824
  // `path` is the display filename per the binary-attachment projection
@@ -39755,7 +40099,7 @@ async function exportShardBytes(db, options, mode) {
39755
40099
  parameters: jsonObject3(row.parameters_json),
39756
40100
  input_hash: row.input_hash,
39757
40101
  freshness: jsonObject3(row.freshness_json) ?? { status: "unknown" },
39758
- created_at: iso2(row.created_at)
40102
+ created_at: iso3(row.created_at)
39759
40103
  }));
39760
40104
  files.set("graph_sources.json", encoder5.encode(JSON.stringify(shardGraphSources)));
39761
40105
  components4.push("graph_sources");
@@ -39807,7 +40151,7 @@ async function exportShardBytes(db, options, mode) {
39807
40151
  representative_note_ids: community.representative_note_ids ?? [],
39808
40152
  metadata: jsonObject3(community.metadata_json)
39809
40153
  })),
39810
- created_at: iso2(row.created_at)
40154
+ created_at: iso3(row.created_at)
39811
40155
  }));
39812
40156
  files.set("communities.json", encoder5.encode(JSON.stringify(shardCommunitySets)));
39813
40157
  components4.push("communities");
@@ -39840,7 +40184,7 @@ async function exportShardBytes(db, options, mode) {
39840
40184
  if (!tagsByName.has(name)) {
39841
40185
  tagsByName.set(name, {
39842
40186
  name,
39843
- created_at: iso2(template.created_at)
40187
+ created_at: iso3(template.created_at)
39844
40188
  });
39845
40189
  }
39846
40190
  }
@@ -40281,6 +40625,7 @@ async function importNativeFullV1(db, data, options = {}) {
40281
40625
  }
40282
40626
  const sidecars = collectSidecarBlobs(files);
40283
40627
  let promotion;
40628
+ const reerasedOperationIds = [];
40284
40629
  try {
40285
40630
  let appliedCounts = {};
40286
40631
  await db.transaction(async (tx) => {
@@ -40312,8 +40657,29 @@ async function importNativeFullV1(db, data, options = {}) {
40312
40657
  });
40313
40658
  appliedCounts = stateCounts(selected);
40314
40659
  await writeNativeLineage(tx, selected, manifest, components3.every((component) => state[component].length === 0));
40660
+ const reerased = await reeraseLifecyclePurgeTargets(
40661
+ tx,
40662
+ selected.notes.map((note) => note.id),
40663
+ Boolean(options.blobStore)
40664
+ );
40665
+ reerasedOperationIds.push(...reerased.operation_ids);
40666
+ if (reerased.note_ids.length > 0) {
40667
+ warnings.push(
40668
+ `${reerased.note_ids.length} retained lifecycle purge target(s) were re-erased before indexing.`
40669
+ );
40670
+ }
40315
40671
  await options.onProgress?.({ phase: "index", done: 0, total: 1 });
40316
40672
  });
40673
+ if (options.blobStore) {
40674
+ const lifecycle = new LifecyclePurgeRepository(db, void 0, options.blobStore);
40675
+ for (const operationId of reerasedOperationIds) {
40676
+ try {
40677
+ await lifecycle.resume(operationId);
40678
+ } catch {
40679
+ warnings.push("A restored purge target was re-erased, but byte cleanup remains pending.");
40680
+ }
40681
+ }
40682
+ }
40317
40683
  try {
40318
40684
  await options.onProgress?.({ phase: "index", done: 1, total: 1 });
40319
40685
  } catch {
@@ -40453,6 +40819,7 @@ async function importShard(db, data, options) {
40453
40819
  });
40454
40820
  const inputData = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
40455
40821
  let files;
40822
+ const reerasedOperationIds = [];
40456
40823
  try {
40457
40824
  await report?.({ phase: "unpack", done: 0, total: 1 });
40458
40825
  files = unpackTarGz(inputData);
@@ -41611,6 +41978,17 @@ async function importShard(db, data, options) {
41611
41978
  report?.({ phase: "embedding_set_members", done: index + 1, total: parsedEmbMembers.length });
41612
41979
  await maybeYield2(index + 1, batchSize);
41613
41980
  }
41981
+ const reerased = await reeraseLifecyclePurgeTargets(
41982
+ tx,
41983
+ parsedNotes.map((note) => note.id),
41984
+ Boolean(options?.blobStore)
41985
+ );
41986
+ reerasedOperationIds.push(...reerased.operation_ids);
41987
+ if (reerased.note_ids.length > 0) {
41988
+ warnings.push(
41989
+ `${reerased.note_ids.length} retained lifecycle purge target(s) were re-erased before indexing.`
41990
+ );
41991
+ }
41614
41992
  });
41615
41993
  report?.({ phase: "index", done: 1, total: 1 });
41616
41994
  } catch (err) {
@@ -41633,6 +42011,16 @@ async function importShard(db, data, options) {
41633
42011
  capability_report: capabilityReport
41634
42012
  };
41635
42013
  }
42014
+ if (options?.blobStore) {
42015
+ const lifecycle = new LifecyclePurgeRepository(db, void 0, options.blobStore);
42016
+ for (const operationId of reerasedOperationIds) {
42017
+ try {
42018
+ await lifecycle.resume(operationId);
42019
+ } catch {
42020
+ warnings.push("A restored purge target was re-erased, but byte cleanup remains pending.");
42021
+ }
42022
+ }
42023
+ }
41636
42024
  return {
41637
42025
  success: true,
41638
42026
  counts,
@@ -46306,7 +46694,11 @@ var RECORD_COLLECTIONS = [
46306
46694
  "source_identity",
46307
46695
  "source_import_run",
46308
46696
  "source_import_batch",
46309
- "deletion_receipt"
46697
+ "deletion_receipt",
46698
+ "lifecycle_purge_preview",
46699
+ "lifecycle_purge_operation",
46700
+ "lifecycle_purge_erasure_target",
46701
+ "lifecycle_purge_blob_cleanup"
46310
46702
  ];
46311
46703
  var RECORD_STORE_CAPABILITIES = {
46312
46704
  crud: true,
@@ -46315,6 +46707,7 @@ var RECORD_STORE_CAPABILITIES = {
46315
46707
  boundedTextScan: true,
46316
46708
  sourceAddressedUpsert: true,
46317
46709
  deletionReceipts: true,
46710
+ purgeJournalCompaction: true,
46318
46711
  typedMetadataPredicates: false,
46319
46712
  evidenceLocators: false,
46320
46713
  fullTextSearch: false,
@@ -46365,6 +46758,12 @@ var MemoryRecordStore = class {
46365
46758
  return (await this.applyBatch([{ op: "delete", collection, id }]))[0];
46366
46759
  }
46367
46760
  async applyBatch(mutations) {
46761
+ return this.commitBatch(mutations, []);
46762
+ }
46763
+ async applyPurgeBatch(mutations, scrubJournal) {
46764
+ return this.commitBatch(mutations, scrubJournal);
46765
+ }
46766
+ async commitBatch(mutations, scrubJournal) {
46368
46767
  if (mutations.length === 0) return [];
46369
46768
  const stagedCollections = /* @__PURE__ */ new Map();
46370
46769
  for (const [collection, records] of this.collections) {
@@ -46373,7 +46772,8 @@ var MemoryRecordStore = class {
46373
46772
  new Map([...records].map(([id, record]) => [id, structuredClone(record)]))
46374
46773
  );
46375
46774
  }
46376
- const stagedJournal = structuredClone(this.journal);
46775
+ const scrub = new Set(scrubJournal.map((key3) => `${key3.collection}\0${key3.id}`));
46776
+ const stagedJournal = structuredClone(this.journal).filter((entry) => !scrub.has(`${entry.collection}\0${entry.id}`));
46377
46777
  let stagedSeq = this.seq;
46378
46778
  const entries = [];
46379
46779
  const table = (collection) => {
@@ -46430,10 +46830,10 @@ var MemoryRecordStore = class {
46430
46830
 
46431
46831
  // src/records/idb-record-store.ts
46432
46832
  init_geometry_buffer();
46433
- var DB_VERSION = 3;
46833
+ var DB_VERSION = 4;
46434
46834
  var JOURNAL_STORE = "journal";
46435
46835
  var META_STORE = "meta";
46436
- var RECORD_SCHEMA_VERSION = 3;
46836
+ var RECORD_SCHEMA_VERSION = 4;
46437
46837
  function requestToPromise(request) {
46438
46838
  return new Promise((resolve, reject) => {
46439
46839
  request.onsuccess = () => resolve(request.result);
@@ -46517,6 +46917,12 @@ var IdbRecordStore = class _IdbRecordStore {
46517
46917
  return (await this.applyBatch([{ op: "delete", collection, id }]))[0];
46518
46918
  }
46519
46919
  async applyBatch(mutations) {
46920
+ return this.commitBatch(mutations, []);
46921
+ }
46922
+ async applyPurgeBatch(mutations, scrubJournal) {
46923
+ return this.commitBatch(mutations, scrubJournal);
46924
+ }
46925
+ async commitBatch(mutations, scrubJournal) {
46520
46926
  if (mutations.length === 0) return [];
46521
46927
  const normalizedMutations = mutations.map(normalizeRecordMutation);
46522
46928
  const collections = [...new Set(normalizedMutations.map((mutation) => mutation.collection))];
@@ -46524,6 +46930,23 @@ var IdbRecordStore = class _IdbRecordStore {
46524
46930
  const completed = transactionComplete(tx);
46525
46931
  const pendingEntries = [];
46526
46932
  try {
46933
+ if (scrubJournal.length > 0) {
46934
+ const scrub = new Set(scrubJournal.map((key3) => `${key3.collection}\0${key3.id}`));
46935
+ await new Promise((resolve, reject) => {
46936
+ const request = tx.objectStore(JOURNAL_STORE).openCursor();
46937
+ request.onerror = () => reject(request.error);
46938
+ request.onsuccess = () => {
46939
+ const cursor = request.result;
46940
+ if (!cursor) {
46941
+ resolve();
46942
+ return;
46943
+ }
46944
+ const entry = cursor.value;
46945
+ if (scrub.has(`${entry.collection}\0${entry.id}`)) cursor.delete();
46946
+ cursor.continue();
46947
+ };
46948
+ });
46949
+ }
46527
46950
  for (const mutation of normalizedMutations) {
46528
46951
  const records = tx.objectStore(mutation.collection);
46529
46952
  const pending = mutation.op === "put" ? {
@@ -46689,6 +47112,8 @@ var CanonicalNotesRepository = class {
46689
47112
  await this.store.put("note", { ...note, deleted_at: nowIso(), updated_at: nowIso() });
46690
47113
  }
46691
47114
  async restore(noteId) {
47115
+ const retained = (await this.store.list("lifecycle_purge_erasure_target")).some((target) => target.note_id === noteId);
47116
+ if (retained) throw new Error("Terminally purged notes cannot be restored");
46692
47117
  const note = await this.store.get("note", noteId);
46693
47118
  if (!note) throw new Error(`Note not found: ${noteId}`);
46694
47119
  await this.store.put("note", { ...note, deleted_at: null, updated_at: nowIso() });
@@ -46922,11 +47347,12 @@ async function projectAttachments(db, store) {
46922
47347
  for (const att of attachments) {
46923
47348
  await db.query(
46924
47349
  `INSERT INTO attachment (
46925
- id, note_id, blob_id, document_type_id, mime_type, extracted_text,
47350
+ id, note_id, blob_id, preview_blob_id, document_type_id, mime_type, extracted_text,
46926
47351
  filename, display_name, position, status, created_at, deleted_at
46927
47352
  )
46928
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
47353
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
46929
47354
  ON CONFLICT (id) DO UPDATE SET
47355
+ preview_blob_id = EXCLUDED.preview_blob_id,
46930
47356
  mime_type = EXCLUDED.mime_type,
46931
47357
  extracted_text = EXCLUDED.extracted_text,
46932
47358
  filename = EXCLUDED.filename,
@@ -46938,6 +47364,7 @@ async function projectAttachments(db, store) {
46938
47364
  att.id,
46939
47365
  att.note_id,
46940
47366
  att.blob_id,
47367
+ att.preview_blob_id ?? null,
46941
47368
  att.document_type_id,
46942
47369
  att.mime_type,
46943
47370
  att.extracted_text,
@@ -46955,7 +47382,7 @@ async function projectAttachments(db, store) {
46955
47382
  `UPDATE attachment_blob ab
46956
47383
  SET reference_count = (
46957
47384
  SELECT COUNT(*) FROM attachment a
46958
- WHERE a.blob_id = ab.id AND a.deleted_at IS NULL
47385
+ WHERE (a.blob_id = ab.id OR a.preview_blob_id = ab.id) AND a.deleted_at IS NULL
46959
47386
  )`
46960
47387
  );
46961
47388
  return { blobs: blobs.length, attachments: attachments.length };
@@ -47310,6 +47737,306 @@ function createRecordBackend(store, options = {}) {
47310
47737
 
47311
47738
  // src/records/record-shard.ts
47312
47739
  init_geometry_buffer();
47740
+
47741
+ // src/records/lifecycle-purge.ts
47742
+ init_geometry_buffer();
47743
+ var RECEIPT_POLICY2 = {
47744
+ authority: "Fortemi/fortemi#1092",
47745
+ mode: "terminal_purge",
47746
+ receipt_contains_content: false,
47747
+ backup_disposition: "beyond_use_then_reerase",
47748
+ restore_reerasure: true
47749
+ };
47750
+ function zeroCounts2() {
47751
+ return {
47752
+ notes: 0,
47753
+ revisions: 0,
47754
+ links: 0,
47755
+ tags: 0,
47756
+ embeddings: 0,
47757
+ attachments: 0,
47758
+ blobs: 0,
47759
+ graph_edges: 0,
47760
+ provenance_edges: 0,
47761
+ source_identities: 0
47762
+ };
47763
+ }
47764
+ function asCounts2(value) {
47765
+ return { ...zeroCounts2(), ...value };
47766
+ }
47767
+ async function selectedNoteIds2(store, selector) {
47768
+ validatePurgeSelector(selector);
47769
+ const notes = await store.list("note");
47770
+ const requested = selector.note_ids ? new Set(selector.note_ids) : null;
47771
+ if (selector.tenant_id === void 0 && selector.archive_id === void 0 && !selector.source) {
47772
+ return notes.filter((note) => requested.has(note.id)).map((note) => note.id).sort();
47773
+ }
47774
+ const identities = await store.list("source_identity");
47775
+ const scoped2 = new Set(identities.filter((identity) => identity.tenant_id === (selector.tenant_id ?? "default") && identity.archive_id === (selector.archive_id ?? null) && (!selector.source || identity.namespace === selector.source.namespace) && (selector.source?.external_id === void 0 || identity.external_id === selector.source.external_id)).map((identity) => identity.note_id));
47776
+ return notes.filter((note) => scoped2.has(note.id) && (!requested || requested.has(note.id))).map((note) => note.id).sort();
47777
+ }
47778
+ function attachmentBlobIds(attachment) {
47779
+ return [attachment.blob_id, attachment.preview_blob_id].filter((id) => Boolean(id));
47780
+ }
47781
+ async function removableBlobs(store, noteSet) {
47782
+ const attachments = await store.list("attachment");
47783
+ const candidates = new Set(attachments.filter((record) => noteSet.has(record.note_id)).flatMap(attachmentBlobIds));
47784
+ for (const record of attachments) {
47785
+ if (!noteSet.has(record.note_id)) {
47786
+ for (const id of attachmentBlobIds(record)) candidates.delete(id);
47787
+ }
47788
+ }
47789
+ return (await store.list("attachment_blob")).filter((record) => candidates.has(record.id));
47790
+ }
47791
+ async function count(store, noteIds) {
47792
+ const counts = zeroCounts2();
47793
+ if (noteIds.length === 0) return counts;
47794
+ const noteSet = new Set(noteIds);
47795
+ counts.notes = (await store.list("note")).filter((record) => noteSet.has(record.id)).length;
47796
+ counts.revisions = (await store.list("note_revision")).filter((record) => noteSet.has(record.note_id)).length;
47797
+ counts.links = (await store.list("link")).filter((record) => noteSet.has(record.source_note_id) || noteSet.has(record.target_note_id)).length;
47798
+ counts.tags = (await store.list("note_tag")).filter((record) => noteSet.has(record.note_id)).length;
47799
+ counts.attachments = (await store.list("attachment")).filter((record) => noteSet.has(record.note_id)).length;
47800
+ counts.blobs = (await removableBlobs(store, noteSet)).length;
47801
+ counts.source_identities = (await store.list("source_identity")).filter((record) => noteSet.has(record.note_id)).length;
47802
+ return counts;
47803
+ }
47804
+ function receiptFromRecord(record) {
47805
+ return {
47806
+ contract_version: LIFECYCLE_PURGE_CONTRACT_VERSION,
47807
+ operation_id: record.operation_id,
47808
+ outcome: "completed",
47809
+ counts: asCounts2(record.counts),
47810
+ completed_at: record.completed_at,
47811
+ policy: record.policy
47812
+ };
47813
+ }
47814
+ async function recordStorePurgeStatus(store, operationId) {
47815
+ assertPurgeOpaqueId(operationId, "operation_id");
47816
+ const operation = await store.get("lifecycle_purge_operation", operationId);
47817
+ if (!operation) return null;
47818
+ const receipt = await store.get("deletion_receipt", operationId);
47819
+ const pending = (await store.list("lifecycle_purge_blob_cleanup")).filter((record) => record.operation_id === operationId && record.completed_at === null).length;
47820
+ return {
47821
+ contract_version: LIFECYCLE_PURGE_CONTRACT_VERSION,
47822
+ operation_id: operationId,
47823
+ outcome: operation.state,
47824
+ counts: asCounts2(operation.counts),
47825
+ blob_cleanup_pending: pending,
47826
+ search_cleanup_pending: false,
47827
+ ...receipt ? { receipt: receiptFromRecord(receipt) } : {}
47828
+ };
47829
+ }
47830
+ async function previewRecordStorePurge(store, selector) {
47831
+ if (!store.applyPurgeBatch) throw new Error("RecordStore purge requires atomic journal-compacting applyPurgeBatch() support");
47832
+ validatePurgeSelector(selector);
47833
+ const now2 = Date.now();
47834
+ const mutations = (await store.list("lifecycle_purge_preview")).filter((record) => record.consumed_by === null && Date.parse(record.expires_at) <= now2).map((record) => ({ op: "delete", collection: "lifecycle_purge_preview", id: record.id }));
47835
+ const noteIds = await selectedNoteIds2(store, selector);
47836
+ const counts = await count(store, noteIds);
47837
+ const previewId = generateId();
47838
+ const expiresAt = new Date(now2 + LIFECYCLE_PURGE_PREVIEW_TTL_SECONDS * 1e3).toISOString();
47839
+ mutations.push({
47840
+ op: "put",
47841
+ collection: "lifecycle_purge_preview",
47842
+ record: {
47843
+ id: previewId,
47844
+ selector_fingerprint: purgeSelectorHash(selector),
47845
+ selector: structuredClone(selector),
47846
+ selected_note_ids: noteIds,
47847
+ counts,
47848
+ expires_at: expiresAt,
47849
+ consumed_by: null
47850
+ }
47851
+ });
47852
+ await store.applyPurgeBatch(
47853
+ mutations,
47854
+ mutations.filter((mutation) => mutation.op === "delete").map((mutation) => ({ collection: mutation.collection, id: mutation.id }))
47855
+ );
47856
+ return {
47857
+ contract_version: LIFECYCLE_PURGE_CONTRACT_VERSION,
47858
+ preview_id: previewId,
47859
+ counts,
47860
+ expires_at: expiresAt
47861
+ };
47862
+ }
47863
+ async function graphDeletionMutations(store, noteIds, deleteSourceJournals) {
47864
+ const noteSet = new Set(noteIds);
47865
+ const mutations = [];
47866
+ for (const collection of [
47867
+ "note_revised_current",
47868
+ "note_original",
47869
+ "note_revision",
47870
+ "note_tag",
47871
+ "collection_note",
47872
+ "attachment",
47873
+ "source_identity"
47874
+ ]) {
47875
+ for (const record of await store.list(collection)) {
47876
+ const noteId = collection === "note_revised_current" ? record.id : "note_id" in record && typeof record.note_id === "string" ? record.note_id : null;
47877
+ if (noteId && noteSet.has(noteId)) mutations.push({ op: "delete", collection, id: record.id });
47878
+ }
47879
+ }
47880
+ for (const record of await store.list("link")) {
47881
+ if (noteSet.has(record.source_note_id) || noteSet.has(record.target_note_id)) {
47882
+ mutations.push({ op: "delete", collection: "link", id: record.id });
47883
+ }
47884
+ }
47885
+ for (const note of await store.list("note")) {
47886
+ if (noteSet.has(note.id)) mutations.push({ op: "delete", collection: "note", id: note.id });
47887
+ }
47888
+ const blobs = await removableBlobs(store, noteSet);
47889
+ for (const blob of blobs) mutations.push({ op: "delete", collection: "attachment_blob", id: blob.id });
47890
+ if (deleteSourceJournals) {
47891
+ const selectedIdentities = (await store.list("source_identity")).filter((identity) => noteSet.has(identity.note_id));
47892
+ const survivors = (await store.list("source_identity")).filter((identity) => !noteSet.has(identity.note_id));
47893
+ const scopeKey = (record) => JSON.stringify([record.tenant_id, record.archive_id, record.namespace]);
47894
+ const emptiedScopes = new Set(selectedIdentities.map(scopeKey).filter((scope) => !survivors.some((item) => scopeKey(item) === scope)));
47895
+ for (const run of await store.list("source_import_run")) {
47896
+ if (emptiedScopes.has(scopeKey(run))) mutations.push({ op: "delete", collection: "source_import_run", id: run.id });
47897
+ }
47898
+ for (const batch of await store.list("source_import_batch")) {
47899
+ if (emptiedScopes.has(scopeKey(batch))) mutations.push({ op: "delete", collection: "source_import_batch", id: batch.id });
47900
+ }
47901
+ }
47902
+ return { mutations, blobs };
47903
+ }
47904
+ function terminalRecords(operation, completedAt) {
47905
+ const completed = { ...operation, state: "completed", completed_at: completedAt };
47906
+ return {
47907
+ operation: completed,
47908
+ receipt: {
47909
+ id: operation.id,
47910
+ contract_version: LIFECYCLE_PURGE_CONTRACT_VERSION,
47911
+ operation_id: operation.id,
47912
+ outcome: "completed",
47913
+ counts: operation.counts,
47914
+ completed_at: completedAt,
47915
+ policy: RECEIPT_POLICY2
47916
+ }
47917
+ };
47918
+ }
47919
+ async function beginRecordStorePurge(store, request, blobStore) {
47920
+ if (!store.applyPurgeBatch) throw new Error("RecordStore purge requires atomic journal-compacting applyPurgeBatch() support");
47921
+ assertPurgeOpaqueId(request.operation_id, "operation_id");
47922
+ assertPurgeOpaqueId(request.preview_id, "preview_id");
47923
+ const prior = await store.get("lifecycle_purge_operation", request.operation_id);
47924
+ if (prior) {
47925
+ if (prior.preview_id !== request.preview_id) throw new Error("Purge operation ID was already used for another preview");
47926
+ return await recordStorePurgeStatus(store, request.operation_id);
47927
+ }
47928
+ const preview = await store.get("lifecycle_purge_preview", request.preview_id);
47929
+ if (!preview) throw new Error("Lifecycle purge preview was not found");
47930
+ if (preview.consumed_by !== null) throw new Error("Lifecycle purge preview was already consumed");
47931
+ if (Date.parse(preview.expires_at) <= Date.now()) throw new Error("Lifecycle purge preview expired; create a new preview");
47932
+ const selector = preview.selector;
47933
+ validatePurgeSelector(selector);
47934
+ if (purgeSelectorHash(selector) !== preview.selector_fingerprint) throw new Error("Lifecycle purge preview integrity check failed");
47935
+ const noteIds = await selectedNoteIds2(store, selector);
47936
+ const counts = await count(store, noteIds);
47937
+ if (JSON.stringify(noteIds) !== JSON.stringify(preview.selected_note_ids) || JSON.stringify(counts) !== JSON.stringify(asCounts2(preview.counts))) {
47938
+ throw new Error("Lifecycle purge preview is stale; create a new preview");
47939
+ }
47940
+ const deletion = await graphDeletionMutations(
47941
+ store,
47942
+ noteIds,
47943
+ selector.source !== void 0 && selector.source.external_id === void 0
47944
+ );
47945
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
47946
+ let operation = {
47947
+ id: request.operation_id,
47948
+ preview_id: request.preview_id,
47949
+ selector_fingerprint: preview.selector_fingerprint,
47950
+ state: blobStore && deletion.blobs.length > 0 ? "cleanup_pending" : "completed",
47951
+ counts,
47952
+ created_at: now2,
47953
+ completed_at: null
47954
+ };
47955
+ const mutations = [...deletion.mutations];
47956
+ for (const noteId of noteIds) {
47957
+ mutations.push({
47958
+ op: "put",
47959
+ collection: "lifecycle_purge_erasure_target",
47960
+ record: { id: `${request.operation_id}\0${noteId}`, operation_id: request.operation_id, note_id: noteId }
47961
+ });
47962
+ }
47963
+ if (blobStore) {
47964
+ for (const blob of deletion.blobs) {
47965
+ mutations.push({
47966
+ op: "put",
47967
+ collection: "lifecycle_purge_blob_cleanup",
47968
+ record: {
47969
+ id: `${request.operation_id}\0${blob.id}`,
47970
+ operation_id: request.operation_id,
47971
+ blob_id: blob.id,
47972
+ content_hash: blob.content_hash,
47973
+ completed_at: null
47974
+ }
47975
+ });
47976
+ }
47977
+ }
47978
+ if (operation.state === "completed") {
47979
+ const terminal = terminalRecords(operation, now2);
47980
+ operation = terminal.operation;
47981
+ mutations.push({ op: "put", collection: "deletion_receipt", record: terminal.receipt });
47982
+ }
47983
+ mutations.push({ op: "put", collection: "lifecycle_purge_operation", record: operation });
47984
+ mutations.push({
47985
+ op: "put",
47986
+ collection: "lifecycle_purge_preview",
47987
+ record: { ...preview, selector: {}, selected_note_ids: [], consumed_by: request.operation_id }
47988
+ });
47989
+ const scrubJournal = deletion.mutations.filter((mutation) => mutation.op === "delete").map((mutation) => ({ collection: mutation.collection, id: mutation.id }));
47990
+ scrubJournal.push({ collection: "lifecycle_purge_preview", id: request.preview_id });
47991
+ await store.applyPurgeBatch(mutations, scrubJournal);
47992
+ return await recordStorePurgeStatus(store, request.operation_id);
47993
+ }
47994
+ async function resumeRecordStorePurge(store, operationId, blobStore) {
47995
+ if (!store.applyBatch) throw new Error("RecordStore purge requires atomic applyBatch() support");
47996
+ assertPurgeOpaqueId(operationId, "operation_id");
47997
+ const status = await recordStorePurgeStatus(store, operationId);
47998
+ if (!status) throw new Error("Lifecycle purge operation was not found");
47999
+ if (status.outcome === "completed") return status;
48000
+ if (!blobStore?.delete) throw new Error("Lifecycle purge byte cleanup requires BlobStore.delete()");
48001
+ const pending = (await store.list("lifecycle_purge_blob_cleanup")).filter((record) => record.operation_id === operationId && record.completed_at === null);
48002
+ for (const blob of pending) {
48003
+ await blobStore.delete(blob.content_hash);
48004
+ await store.put("lifecycle_purge_blob_cleanup", { ...blob, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
48005
+ }
48006
+ const operation = await store.get("lifecycle_purge_operation", operationId);
48007
+ if (!operation) throw new Error("Lifecycle purge operation was not found");
48008
+ const terminal = terminalRecords(operation, (/* @__PURE__ */ new Date()).toISOString());
48009
+ await store.applyBatch([
48010
+ { op: "put", collection: "lifecycle_purge_operation", record: terminal.operation },
48011
+ { op: "put", collection: "deletion_receipt", record: terminal.receipt }
48012
+ ]);
48013
+ return await recordStorePurgeStatus(store, operationId);
48014
+ }
48015
+ async function purgeRecordStoreGraph(store, selector, operationId, blobStore) {
48016
+ validatePurgeSelector(selector);
48017
+ assertPurgeOpaqueId(operationId, "operation_id");
48018
+ const existing = await store.get("lifecycle_purge_operation", operationId);
48019
+ if (existing) {
48020
+ if (existing.selector_fingerprint !== purgeSelectorHash(selector)) {
48021
+ throw new Error("Purge operation key conflicts with selector");
48022
+ }
48023
+ let replay = await recordStorePurgeStatus(store, operationId);
48024
+ if (replay.outcome === "cleanup_pending") replay = await resumeRecordStorePurge(store, operationId, blobStore);
48025
+ if (!replay.receipt) throw new Error("Lifecycle purge did not produce a terminal receipt");
48026
+ return replay.receipt;
48027
+ }
48028
+ const preview = await previewRecordStorePurge(store, selector);
48029
+ let status = await beginRecordStorePurge(store, { operation_id: operationId, preview_id: preview.preview_id }, blobStore);
48030
+ if (status.outcome === "cleanup_pending") status = await resumeRecordStorePurge(store, operationId, blobStore);
48031
+ if (!status.receipt) throw new Error("Lifecycle purge did not produce a terminal receipt");
48032
+ return status.receipt;
48033
+ }
48034
+ async function recordStoreErasureTargetIds(store, candidateNoteIds) {
48035
+ const candidates = candidateNoteIds ? new Set(candidateNoteIds) : null;
48036
+ return new Set((await store.list("lifecycle_purge_erasure_target")).filter((record) => !candidates || candidates.has(record.note_id)).map((record) => record.note_id));
48037
+ }
48038
+
48039
+ // src/records/record-shard.ts
47313
48040
  var encoder8 = new TextEncoder();
47314
48041
  var decoder9 = new TextDecoder();
47315
48042
  function emptyCounts4() {
@@ -47983,6 +48710,17 @@ async function importShardToRecords(store, data, options) {
47983
48710
  capabilityReport
47984
48711
  );
47985
48712
  }
48713
+ const retainedTargets = await recordStoreErasureTargetIds(
48714
+ store,
48715
+ parsedNotes.map((note) => note.id)
48716
+ );
48717
+ if (retainedTargets.size > 0) {
48718
+ parsedNotes = parsedNotes.filter((note) => !retainedTargets.has(note.id));
48719
+ parsedLinks = parsedLinks.filter((link2) => !retainedTargets.has(link2.from_note_id) && (link2.to_note_id === null || !retainedTargets.has(link2.to_note_id)));
48720
+ warnings.push(
48721
+ `${retainedTargets.size} retained lifecycle purge target(s) were re-erased before record import.`
48722
+ );
48723
+ }
47986
48724
  for (const component of manifest.components ?? []) {
47987
48725
  if (UNSUPPORTED_COMPONENTS.includes(component)) {
47988
48726
  const count2 = manifest.counts?.[component];
@@ -48671,125 +49409,8 @@ function validate3(items, maxItems, initialReason) {
48671
49409
  }));
48672
49410
  }
48673
49411
 
48674
- // src/records/lifecycle-purge.ts
48675
- init_geometry_buffer();
48676
- function hashSelector(selector) {
48677
- return computeHash(new TextEncoder().encode(JSON.stringify({
48678
- tenant_id: selector.tenant_id ?? "default",
48679
- archive_id: selector.archive_id ?? null,
48680
- note_ids: [...selector.note_ids ?? []].sort(),
48681
- source: selector.source ? {
48682
- namespace: selector.source.namespace,
48683
- external_id_hash: selector.source.external_id ? computeHash(new TextEncoder().encode(selector.source.external_id)) : null
48684
- } : null
48685
- })));
48686
- }
48687
- function zeroCounts2() {
48688
- return {
48689
- notes: 0,
48690
- revisions: 0,
48691
- links: 0,
48692
- tags: 0,
48693
- embeddings: 0,
48694
- attachments: 0,
48695
- blobs: 0,
48696
- graph_edges: 0,
48697
- provenance_edges: 0,
48698
- source_identities: 0
48699
- };
48700
- }
48701
- async function selectedNoteIds2(store, selector) {
48702
- const notes = await store.list("note");
48703
- if (selector.note_ids?.length) return notes.filter((note) => selector.note_ids.includes(note.id)).map((note) => note.id);
48704
- if (!selector.source) throw new Error("Purge selector must target note_ids or source identity");
48705
- const identities = await store.list("source_identity");
48706
- return identities.filter((identity) => identity.tenant_id === (selector.tenant_id ?? "default") && identity.archive_id === (selector.archive_id ?? null) && identity.namespace === selector.source.namespace && (selector.source.external_id === void 0 || identity.external_id === selector.source.external_id)).map((identity) => identity.note_id);
48707
- }
48708
- async function count(store, noteIds) {
48709
- const counts = zeroCounts2();
48710
- if (noteIds.length === 0) return counts;
48711
- const noteSet = new Set(noteIds);
48712
- counts.notes = (await store.list("note")).filter((record) => noteSet.has(record.id)).length;
48713
- counts.revisions = 0;
48714
- counts.links = (await store.list("link")).filter((record) => noteSet.has(record.source_note_id) || noteSet.has(record.target_note_id)).length;
48715
- counts.tags = (await store.list("note_tag")).filter((record) => noteSet.has(record.note_id)).length;
48716
- counts.attachments = (await store.list("attachment")).filter((record) => noteSet.has(record.note_id)).length;
48717
- const purgedBlobIds = new Set((await store.list("attachment")).filter((record) => noteSet.has(record.note_id)).map((record) => record.blob_id));
48718
- counts.blobs = (await store.list("attachment_blob")).filter((record) => purgedBlobIds.has(record.id)).length;
48719
- counts.source_identities = (await store.list("source_identity")).filter((record) => noteSet.has(record.note_id)).length;
48720
- return counts;
48721
- }
48722
- async function previewRecordStorePurge(store, selector) {
48723
- return { selector_hash: hashSelector(selector), counts: await count(store, await selectedNoteIds2(store, selector)) };
48724
- }
48725
- async function purgeRecordStoreGraph(store, selector, operationKey) {
48726
- if (!store.applyBatch) throw new Error("RecordStore purge requires atomic applyBatch() support");
48727
- const prior = (await store.list("deletion_receipt")).find((receipt2) => receipt2.operation_key === operationKey);
48728
- if (prior) return {
48729
- id: prior.id,
48730
- operation_key: prior.operation_key,
48731
- tenant_id: prior.tenant_id,
48732
- archive_id: prior.archive_id,
48733
- selector_hash: prior.selector_hash,
48734
- outcome: "completed",
48735
- counts: prior.counts,
48736
- completed_at: prior.completed_at,
48737
- policy: prior.policy
48738
- };
48739
- const noteIds = await selectedNoteIds2(store, selector);
48740
- const noteSet = new Set(noteIds);
48741
- const counts = await count(store, noteIds);
48742
- const mutations = [];
48743
- for (const collection of ["note_revised_current", "note_original", "note_tag", "collection_note", "attachment", "source_identity"]) {
48744
- for (const record of await store.list(collection)) {
48745
- const noteId = collection === "note_revised_current" ? record.id : "note_id" in record && typeof record.note_id === "string" ? record.note_id : null;
48746
- if (noteId && noteSet.has(noteId)) {
48747
- mutations.push({ op: "delete", collection, id: record.id });
48748
- }
48749
- }
48750
- }
48751
- for (const record of await store.list("link")) {
48752
- if (noteSet.has(record.source_note_id) || noteSet.has(record.target_note_id)) mutations.push({ op: "delete", collection: "link", id: record.id });
48753
- }
48754
- for (const record of await store.list("note")) {
48755
- if (noteSet.has(record.id)) mutations.push({ op: "delete", collection: "note", id: record.id });
48756
- }
48757
- const liveBlobIds = new Set((await store.list("attachment")).filter((record) => !noteSet.has(record.note_id)).map((record) => record.blob_id));
48758
- for (const record of await store.list("attachment_blob")) {
48759
- if (!liveBlobIds.has(record.id)) mutations.push({ op: "delete", collection: "attachment_blob", id: record.id });
48760
- }
48761
- const receipt = {
48762
- id: generateId(),
48763
- operation_key: operationKey,
48764
- tenant_id: selector.tenant_id ?? "default",
48765
- archive_id: selector.archive_id ?? null,
48766
- selector_hash: hashSelector(selector),
48767
- outcome: "completed",
48768
- counts,
48769
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
48770
- policy: {
48771
- authority: "fortemi#1092",
48772
- mode: "terminal-purge",
48773
- receipt_contains_content: false
48774
- }
48775
- };
48776
- mutations.push({ op: "put", collection: "deletion_receipt", record: receipt });
48777
- await store.applyBatch(mutations);
48778
- return {
48779
- id: receipt.id,
48780
- operation_key: receipt.operation_key,
48781
- tenant_id: receipt.tenant_id,
48782
- archive_id: receipt.archive_id,
48783
- selector_hash: receipt.selector_hash,
48784
- outcome: "completed",
48785
- counts,
48786
- completed_at: receipt.completed_at,
48787
- policy: receipt.policy
48788
- };
48789
- }
48790
-
48791
49412
  // src/index.ts
48792
- var VERSION = "2026.9.6";
49413
+ var VERSION = "2026.9.7";
48793
49414
  /*! Bundled license information:
48794
49415
 
48795
49416
  ieee754/index.js:
@@ -48804,6 +49425,6 @@ buffer/index.js:
48804
49425
  *)
48805
49426
  */
48806
49427
 
48807
- export { AIWG_SCAN_REQUIRED_FIELDS, AllowlistTrustStore, ArchiveManager, AttachmentsRepository, BridgeInferenceProvider, CORE_V1_COMPONENTS, CURRENT_MIGRATION_HEAD, CURRENT_SHARD_VERSION, CanonicalAttachmentsRepository, CanonicalNotesRepository, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, DATASET_EXECUTION_CAPABILITY_IDS, DATASET_EXECUTION_CONTRACT, DATASET_EXECUTION_SCHEMA_VERSION, DATASET_INGEST_CONTRACT, DATASET_INGEST_SCHEMA_VERSION, DATASET_LINEAGE_CONTRACT, DATASET_LINEAGE_SCHEMA_VERSION, DATASET_MATERIALIZATION_CONTRACT, DATASET_MATERIALIZATION_KINDS, DATASET_MATERIALIZATION_SCHEMA_VERSION, DB_SNAPSHOT_SCHEMA_VERSION, DEFAULT_LARGE_DOCUMENT_CHARS, DEFAULT_LARGE_DOCUMENT_CHUNKS, DatasetIngestError, DatasetIngestExecutor, DatasetLineageLedger, DatasetMaterializationError, DbSnapshotVersionError, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FORTEMI_BROWSER_LOCAL_DATASET_EXECUTION_DESCRIPTOR, FORTEMI_COMPATIBILITY_PATH, FORTEMI_COMPATIBILITY_STATES, FORTEMI_PORTABLE_SHARD_DATASET_EXECUTION_DESCRIPTOR, FORTEMI_REQUIRED_COMPATIBILITY_CAPABILITIES, FORTEMI_SERVER_COMPATIBILITY_REVISION, FORTEMI_STATIC_CACHE_DATASET_EXECUTION_DESCRIPTOR, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, IdbRecordStore, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LINEAGE_ENTITY_KINDS, LINEAGE_RELATIONSHIP_KINDS, LOCAL_ENDPOINTS, LifecyclePurgeRepository, LineageValidationError, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MemoryDatasetIngestStore, MemoryRecordStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProvenanceRepository, ProviderRegistry, RECORD_COLLECTIONS, RECORD_SCHEMA_VERSION, RECORD_STORE_CAPABILITIES, REGISTERED_METADATA_PATHS, RemoteBackendError, SHARD_FORMAT, SIGNATURE_ENTRY, SIGNING_ENVELOPE_VERSION, SOURCE_UPSERT_CONTRACT_VERSION, SOURCE_UPSERT_MAX_ITEMS, SUPPORTED_PGLITE_VERSION, SearchInputSchema, SearchRepository, SkosRepository, SourceUpsertRepository, TagsRepository, TemplatesRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexFromKnowledgeShard, aiwgFortemiIndexToCommunityGraph, aiwgFortemiIndexToKnowledgeShard, aiwgFortemiIndexToKnowledgeShardWithReport, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, assertShardComponentRecord, bindSearchEvidence, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, buildMetadataPredicateConditions, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, compareDatasetIncrementalParity, computeBlobHash, computeHash, computeLineageDigest, computeSri, conceptTaggingHandler, configureInferenceRuntime, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createBridgeInferenceProviders, createCosineSemanticProvider, createCspReportHandler, createFortemi, createLazyBlobStore, createLegacyProvider, createLocalProviderProfile, createPGliteBackend, createPGliteInstance, createRecordBackend, createRecordStore, createRemoteBackend, createRoutes, createSearchEvidenceSet, createShardBackend, createShardCapabilityReport, createWorkerEmbedFunction, datasetDestinationScopeKey, defaultStorageBackendFactory, defineInferenceProvider, defineInferenceRuntime, defineLegacyInferenceProvider, defineOpenAICompatibleProvider, deriveDatasetIngestIdempotencyKey, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, digestDatasetMaterializationValue, discoverLocalProviders, dropAttachmentProjection, dropNoteProjection, dumpDbSnapshot, embeddingConfigToShard, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, executeDatasetMaterialization, executeDatasetRetrieval, exportFullV1Snapshot, exportShard, exportShardFromRecords, exportShardFromRecordsWithReport, exportShardWithReport, fetchAndValidateFortemiCompatibility, fetchPluginScript, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, formatFortemiCompatibilitySummary, fortemiCompatibilityUrl, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getAiwgFortemiIndexExportSchema, getConfiguredInferenceProviderId, getEmbedFunction, getEmbeddingTaskSelectionOptions, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getKnowledgeShardContractReceipt, getKnowledgeShardProfileRegistry, getKnowledgeShardSchema, getLlmFunction, getNote, getPrefetchedSha256, getProviderRouteRequirementIssue, handleEmbedRequests, hasFortemiSecureSecrets, importFullV1Snapshot, importShard, importShardToRecords, inferInferenceTaskCapability, inferLocalEmbeddingDimensions, isPluginScriptAllowed, isShardPrefetched, isShardSigningSupported, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, mergeInferenceRuntimeConfigs, mergeSearchEvidenceSets, migrateLegacyBlobStore, negotiateDatasetExecutionCapabilities, negotiateDatasetExecutionCapabilitiesFromWire, negotiateDatasetMaterializationProfile, noteFromShard, noteSkosTagToShard, noteToShard, openShard, packTarGz, parseCspReport, parseSearchEvidenceLocator, parseSearchEvidenceSet, prefetchShard, previewRecordStorePurge, profileSupportError, projectAttachments, projectNotes, projectRecords, provenanceEdgeToShard, providerSatisfiesRouteRequirements, purgeRecordStoreGraph, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, resolveSearchEvidence, restoreDbSnapshot, searchTool, selectBackend, selectEmbeddingTask, selectLlmModel, setEmbedFunction, setEmbeddingTaskSelectionOptions, setLlmFunction, sha256Hex, sidecarBlobDigests, signShard, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsToShard, templateToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, upsertRecordStoreRequest, upsertRecordStoreSources, urlLinkToShard, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgFortemiIndexExportSchema, validateAiwgFortemiProjectedRecordSchema, validateAiwgStaticEmbeddingSet, validateChecksums, validateCoreV1ShardArchive, validateDatasetBenchmarkEvidence, validateDatasetExecutionDescriptor, validateDatasetExecutionRequest, validateFortemiCompatibilityResponse, validateFullV1ShardArchive, validateProviderRoute, validateRecordV1ShardArchive, validateShardArchive, validateShardComponentRecord, validateShardManifest, verifyDbSnapshotMeta, verifyShardSignature, verifySri };
49428
+ export { AIWG_SCAN_REQUIRED_FIELDS, AllowlistTrustStore, ArchiveManager, AttachmentsRepository, BridgeInferenceProvider, CORE_V1_COMPONENTS, CURRENT_MIGRATION_HEAD, CURRENT_SHARD_VERSION, CanonicalAttachmentsRepository, CanonicalNotesRepository, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, DATASET_EXECUTION_CAPABILITY_IDS, DATASET_EXECUTION_CONTRACT, DATASET_EXECUTION_SCHEMA_VERSION, DATASET_INGEST_CONTRACT, DATASET_INGEST_SCHEMA_VERSION, DATASET_LINEAGE_CONTRACT, DATASET_LINEAGE_SCHEMA_VERSION, DATASET_MATERIALIZATION_CONTRACT, DATASET_MATERIALIZATION_KINDS, DATASET_MATERIALIZATION_SCHEMA_VERSION, DB_SNAPSHOT_SCHEMA_VERSION, DEFAULT_LARGE_DOCUMENT_CHARS, DEFAULT_LARGE_DOCUMENT_CHUNKS, DatasetIngestError, DatasetIngestExecutor, DatasetLineageLedger, DatasetMaterializationError, DbSnapshotVersionError, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FORTEMI_BROWSER_LOCAL_DATASET_EXECUTION_DESCRIPTOR, FORTEMI_COMPATIBILITY_PATH, FORTEMI_COMPATIBILITY_STATES, FORTEMI_PORTABLE_SHARD_DATASET_EXECUTION_DESCRIPTOR, FORTEMI_REQUIRED_COMPATIBILITY_CAPABILITIES, FORTEMI_SERVER_COMPATIBILITY_REVISION, FORTEMI_STATIC_CACHE_DATASET_EXECUTION_DESCRIPTOR, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, IdbRecordStore, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LIFECYCLE_PURGE_CONTRACT_VERSION, LIFECYCLE_PURGE_MAX_NOTE_IDS, LIFECYCLE_PURGE_PREVIEW_TTL_SECONDS, LINEAGE_ENTITY_KINDS, LINEAGE_RELATIONSHIP_KINDS, LOCAL_ENDPOINTS, LifecyclePurgeRepository, LineageValidationError, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MemoryDatasetIngestStore, MemoryRecordStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProvenanceRepository, ProviderRegistry, RECORD_COLLECTIONS, RECORD_SCHEMA_VERSION, RECORD_STORE_CAPABILITIES, REGISTERED_METADATA_PATHS, RemoteBackendError, SHARD_FORMAT, SIGNATURE_ENTRY, SIGNING_ENVELOPE_VERSION, SOURCE_UPSERT_CONTRACT_VERSION, SOURCE_UPSERT_MAX_ITEMS, SUPPORTED_PGLITE_VERSION, SearchInputSchema, SearchRepository, SkosRepository, SourceUpsertRepository, TagsRepository, TemplatesRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexFromKnowledgeShard, aiwgFortemiIndexToCommunityGraph, aiwgFortemiIndexToKnowledgeShard, aiwgFortemiIndexToKnowledgeShardWithReport, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, assertShardComponentRecord, beginRecordStorePurge, bindSearchEvidence, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, buildMetadataPredicateConditions, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, compareDatasetIncrementalParity, computeBlobHash, computeHash, computeLineageDigest, computeSri, conceptTaggingHandler, configureInferenceRuntime, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createBridgeInferenceProviders, createCosineSemanticProvider, createCspReportHandler, createFortemi, createLazyBlobStore, createLegacyProvider, createLocalProviderProfile, createPGliteBackend, createPGliteInstance, createRecordBackend, createRecordStore, createRemoteBackend, createRoutes, createSearchEvidenceSet, createShardBackend, createShardCapabilityReport, createWorkerEmbedFunction, datasetDestinationScopeKey, defaultStorageBackendFactory, defineInferenceProvider, defineInferenceRuntime, defineLegacyInferenceProvider, defineOpenAICompatibleProvider, deriveDatasetIngestIdempotencyKey, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, digestDatasetMaterializationValue, discoverLocalProviders, dropAttachmentProjection, dropNoteProjection, dumpDbSnapshot, embeddingConfigToShard, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, executeDatasetMaterialization, executeDatasetRetrieval, exportFullV1Snapshot, exportShard, exportShardFromRecords, exportShardFromRecordsWithReport, exportShardWithReport, fetchAndValidateFortemiCompatibility, fetchPluginScript, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, formatFortemiCompatibilitySummary, fortemiCompatibilityUrl, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getAiwgFortemiIndexExportSchema, getConfiguredInferenceProviderId, getEmbedFunction, getEmbeddingTaskSelectionOptions, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getKnowledgeShardContractReceipt, getKnowledgeShardProfileRegistry, getKnowledgeShardSchema, getLlmFunction, getNote, getPrefetchedSha256, getProviderRouteRequirementIssue, handleEmbedRequests, hasFortemiSecureSecrets, importFullV1Snapshot, importShard, importShardToRecords, inferInferenceTaskCapability, inferLocalEmbeddingDimensions, isPluginScriptAllowed, isShardPrefetched, isShardSigningSupported, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, mergeInferenceRuntimeConfigs, mergeSearchEvidenceSets, migrateLegacyBlobStore, negotiateDatasetExecutionCapabilities, negotiateDatasetExecutionCapabilitiesFromWire, negotiateDatasetMaterializationProfile, noteFromShard, noteSkosTagToShard, noteToShard, openShard, packTarGz, parseCspReport, parseSearchEvidenceLocator, parseSearchEvidenceSet, prefetchShard, previewRecordStorePurge, profileSupportError, projectAttachments, projectNotes, projectRecords, provenanceEdgeToShard, providerSatisfiesRouteRequirements, purgeRecordStoreGraph, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, recordStoreErasureTargetIds, recordStorePurgeStatus, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, resolveSearchEvidence, restoreDbSnapshot, resumeRecordStorePurge, searchTool, selectBackend, selectEmbeddingTask, selectLlmModel, setEmbedFunction, setEmbeddingTaskSelectionOptions, setLlmFunction, sha256Hex, sidecarBlobDigests, signShard, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsToShard, templateToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, upsertRecordStoreRequest, upsertRecordStoreSources, urlLinkToShard, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgFortemiIndexExportSchema, validateAiwgFortemiProjectedRecordSchema, validateAiwgStaticEmbeddingSet, validateChecksums, validateCoreV1ShardArchive, validateDatasetBenchmarkEvidence, validateDatasetExecutionDescriptor, validateDatasetExecutionRequest, validateFortemiCompatibilityResponse, validateFullV1ShardArchive, validateProviderRoute, validateRecordV1ShardArchive, validateShardArchive, validateShardComponentRecord, validateShardManifest, verifyDbSnapshotMeta, verifyShardSignature, verifySri };
48808
49429
  //# sourceMappingURL=index.js.map
48809
49430
  //# sourceMappingURL=index.js.map